diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 565307fd9b7..85d47032d56 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -115,3 +115,47 @@ jobs: with: name: io-helidon-artifacts-${{ needs.create-tag.outputs.version }} path: staging + resolve-all: + needs: [ create-tag, deploy ] + timeout-minutes: 30 + runs-on: ubuntu-20.04 + name: resolve-all + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + ref: ${{ needs.create-tag.outputs.tag }} + - uses: ./.github/actions/common + with: + run: | + mvn ${MAVEN_ARGS} -N \ + -Possrh-staging \ + -Dartifact=io.helidon:helidon-all:${{ needs.create-tag.outputs.version }}:pom \ + dependency:get + smoketest: + needs: [ create-tag, deploy ] + timeout-minutes: 30 + strategy: + matrix: + archetype: + - bare-se + - bare-mp + - quickstart-se + - quickstart-mp + - database-se + - database-mp + runs-on: ubuntu-20.04 + name: smoketest/${{ matrix.archetype }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + ref: ${{ needs.create-tag.outputs.tag }} + - uses: ./.github/actions/common + with: + run: | + ./etc/scripts/smoketest.sh \ + --clean \ + --staged \ + --version=${{ needs.create-tag.outputs.version }} \ + --archetype=${{ matrix.archetype }} diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index ba49f1b425a..ad9cb3a7b00 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -22,6 +22,6 @@ io.helidon.build-tools helidon-build-cache-maven-extension - 4.0.11 + 4.0.12 diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ed779c24a..6487d5857df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,68 @@ For Helidon 2.x releases please see [Helidon 2.x CHANGELOG.md](https://github.co For Helidon 1.x releases please see [Helidon 1.x CHANGELOG.md](https://github.com/oracle/helidon/blob/helidon-1.x/CHANGELOG.md) +## [4.1.2] + +This release contains important bugfixes and enhancements and is recommended for all users of Helidon 4. + +A minimum of Java 21 is required to use Helidon 4. + +### CHANGES + +- gRPC: Adds support to iterate over URIs when connecting to a gRPC service [9300](https://github.com/helidon-io/helidon/pull/9300) +- LRA: LRA testing feature [9320](https://github.com/helidon-io/helidon/pull/9320) +- Logging: JSON Formatter for JUL [9301](https://github.com/helidon-io/helidon/pull/9301) +- Security: Policy validator configurable per endpoint in config (#9248) [9308](https://github.com/helidon-io/helidon/pull/9308) +- WebServer: Allows the webserver's write buffer size to be set to 0 [9314](https://github.com/helidon-io/helidon/pull/9314) +- WebServer: Fix DataReader.findNewLine with lone EOL character [9327](https://github.com/helidon-io/helidon/pull/9327) +- WebServer: Grouping Executors related methods into a single class [9298](https://github.com/helidon-io/helidon/pull/9298) +- WebServer: New implementation for SSE in webserver [9297](https://github.com/helidon-io/helidon/pull/9297) +- WebServer: Smart async writer in webserver [9292](https://github.com/helidon-io/helidon/pull/9292) +- Dependencies: Upgrade Jersey to 3.1.8 [9303](https://github.com/helidon-io/helidon/pull/9303) +- Dependencies: Upgrades protobuf to 3.25.5 [9299](https://github.com/helidon-io/helidon/pull/9299) +- Dependencies: Uptake build-tools 4.0.12 (fixes [9305](https://github.com/helidon-io/helidon/issues/9305)) [9323](https://github.com/helidon-io/helidon/pull/9323) +- Docs: Add emphasis on including an OTel exporter and configuring [9312](https://github.com/helidon-io/helidon/pull/9312) +- Docs: Document work-around for maven archetype issue (#9316) [9324](https://github.com/helidon-io/helidon/pull/9324) +- Tests: Fix DbClient PostgreSQL tests [9293](https://github.com/helidon-io/helidon/pull/9293) + +## [4.1.1] + +This release contains important bugfixes and enhancements and is recommended for all users of Helidon 4. It is compatible with Helidon 4.0.X. + +A minimum of Java 21 is required to use Helidon 4. + +### Notable Changes + +- Implement gRPC MP Client [9026](https://github.com/helidon-io/helidon/pull/9026) + +### CHANGES + +- CORS: Remove headers that do not affect CORS decision-making from request adapter logging output [9178](https://github.com/helidon-io/helidon/pull/9178) +- Codegen: Add support for additional modifiers [9201](https://github.com/helidon-io/helidon/pull/9201) +- Codegen: Fix generation of annotations, including lists, nested annotations etc. [9182](https://github.com/helidon-io/helidon/pull/9182) +- Codegen: Handling enum and type values in a consistent way in code generation. [9167](https://github.com/helidon-io/helidon/pull/9167) +- Codegen: Support for validation of Duration and URI default values. [9166](https://github.com/helidon-io/helidon/pull/9166) +- Codegen: Udpates to types and annotation processing [9168](https://github.com/helidon-io/helidon/pull/9168) +- Config: Replace manual casts on pattern with instanceof in HoconConfigParser [9209](https://github.com/helidon-io/helidon/pull/9209) +- LRA: Replace deprecated method Scheduling.fixedRateBuilder() [9098](https://github.com/helidon-io/helidon/pull/9098) +- Security: Required authorization propagated from the class level now [9137](https://github.com/helidon-io/helidon/pull/9137) +- Tracing: Allow users to direct Helidon to use an existing global `OpenTelemetry` instance rather than create its own [9205](https://github.com/helidon-io/helidon/pull/9205) +- WebServer: Allows the creation of empty SSE events [9207](https://github.com/helidon-io/helidon/pull/9207) +- WebServer: Increases default value of write-buffer-size to 4K [9190](https://github.com/helidon-io/helidon/pull/9190) +- WebServer: UncheckedIOException no longer a special case [9206](https://github.com/helidon-io/helidon/pull/9206) +- gRPC: Downgrades version of protobuf for backwards compatibility [9162](https://github.com/helidon-io/helidon/pull/9162) +- gRPC: Implements support for client gRPC channel injections [9155](https://github.com/helidon-io/helidon/pull/9155) +- gRPC: Implements the gRPC MP Client API [9026](https://github.com/helidon-io/helidon/pull/9026) +- gRPC: Renames package-private Grpc type to GrpcRouteHandler [9173](https://github.com/helidon-io/helidon/pull/9173) +- Build: Fix nightly script [9221](https://github.com/helidon-io/helidon/pull/9221) +- Build: Update release workflows [9210](https://github.com/helidon-io/helidon/pull/9210) +- Dependencies: Upgrade microprofile-cdi-tck to 4.0.13 [9141](https://github.com/helidon-io/helidon/pull/9141) +- Dependencies: Upgrade oci sdk to 3.46.1 [9179](https://github.com/helidon-io/helidon/pull/9179) +- Dependencies: Upgrade slf4j to 2.0.16 [9143](https://github.com/helidon-io/helidon/pull/9143) +- Deprecation: deprecate old injection integration for oci [9184](https://github.com/helidon-io/helidon/pull/9184) +- Docs: Clarify description of config profiles [9188](https://github.com/helidon-io/helidon/pull/9188) +- Docs: Documents gRPC MP Client API [9150](https://github.com/helidon-io/helidon/pull/9150) +- Tests: Builder tests that confidential options are not printed in toString() [9154](https://github.com/helidon-io/helidon/pull/9154) ## [4.1.0] @@ -1397,6 +1459,8 @@ Helidon 4.0.0 is a major release that includes significant new features and fixe - MicroProfile: MP path based static content should use index.html (4.x) [4737](https://github.com/oracle/helidon/pull/4737) - Build: 4.0 version and poms [4655](https://github.com/oracle/helidon/pull/4655) +[4.1.2]: https://github.com/oracle/helidon/compare/4.1.1...4.1.2 +[4.1.1]: https://github.com/oracle/helidon/compare/4.1.0...4.1.1 [4.1.0]: https://github.com/oracle/helidon/compare/4.0.11...4.1.0 [4.0.11]: https://github.com/oracle/helidon/compare/4.0.10...4.0.11 [4.0.10]: https://github.com/oracle/helidon/compare/4.0.9...4.0.10 diff --git a/all/pom.xml b/all/pom.xml index 0abd9ea96f9..009c6cc6f34 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom helidon-all @@ -463,6 +463,10 @@ io.helidon.common.features helidon-common-features + + io.helidon.common.concurrency + helidon-common-concurrency-limits + io.helidon.dbclient helidon-dbclient @@ -996,6 +1000,10 @@ io.helidon.webserver helidon-webserver-service-common + + io.helidon.webserver + helidon-webserver-concurrency-limits + io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5 diff --git a/applications/mp/pom.xml b/applications/mp/pom.xml index 388b5eda632..6be5ed09bce 100644 --- a/applications/mp/pom.xml +++ b/applications/mp/pom.xml @@ -23,7 +23,7 @@ io.helidon.applications helidon-applications - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../parent/pom.xml helidon-mp diff --git a/applications/parent/pom.xml b/applications/parent/pom.xml index 4d7b6975146..a6d26ad323b 100644 --- a/applications/parent/pom.xml +++ b/applications/parent/pom.xml @@ -23,7 +23,7 @@ io.helidon.applications helidon-applications-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.applications helidon-applications @@ -41,8 +41,8 @@ 3.6.0 3.1.0 3.1.2 - 4.0.11 - 4.0.11 + 4.0.14 + 4.0.14 3.3.0 0.10.2 1.5.0.Final diff --git a/applications/pom.xml b/applications/pom.xml index e8a58ec9eb1..14ab1cc7a14 100644 --- a/applications/pom.xml +++ b/applications/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-dependencies - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../dependencies/pom.xml io.helidon.applications diff --git a/applications/se/pom.xml b/applications/se/pom.xml index e3fc3792428..4275245bb9f 100644 --- a/applications/se/pom.xml +++ b/applications/se/pom.xml @@ -23,7 +23,7 @@ io.helidon.applications helidon-applications - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../parent/pom.xml helidon-se diff --git a/archetypes/archetypes/pom.xml b/archetypes/archetypes/pom.xml index 99d392d90e2..c0e7f49f3f3 100644 --- a/archetypes/archetypes/pom.xml +++ b/archetypes/archetypes/pom.xml @@ -23,7 +23,7 @@ io.helidon.archetypes helidon-archetypes-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-archetype helidon-archetypes diff --git a/archetypes/legacy/bare-mp/pom.xml b/archetypes/legacy/bare-mp/pom.xml index 76e5feb9ab0..6d18160010f 100644 --- a/archetypes/legacy/bare-mp/pom.xml +++ b/archetypes/legacy/bare-mp/pom.xml @@ -23,7 +23,7 @@ io.helidon.archetypes helidon-archetypes-legacy-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-bare-mp helidon-archetype diff --git a/archetypes/legacy/bare-se/pom.xml b/archetypes/legacy/bare-se/pom.xml index 4163e47a536..002297b631c 100644 --- a/archetypes/legacy/bare-se/pom.xml +++ b/archetypes/legacy/bare-se/pom.xml @@ -23,7 +23,7 @@ io.helidon.archetypes helidon-archetypes-legacy-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-bare-se helidon-archetype diff --git a/archetypes/legacy/database-mp/pom.xml b/archetypes/legacy/database-mp/pom.xml index f85c680b66c..db52c7cfe81 100644 --- a/archetypes/legacy/database-mp/pom.xml +++ b/archetypes/legacy/database-mp/pom.xml @@ -23,7 +23,7 @@ io.helidon.archetypes helidon-archetypes-legacy-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-database-mp helidon-archetype diff --git a/archetypes/legacy/database-se/pom.xml b/archetypes/legacy/database-se/pom.xml index 633636704a5..0de30d63774 100644 --- a/archetypes/legacy/database-se/pom.xml +++ b/archetypes/legacy/database-se/pom.xml @@ -23,7 +23,7 @@ io.helidon.archetypes helidon-archetypes-legacy-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-database-se helidon-archetype diff --git a/archetypes/legacy/pom.xml b/archetypes/legacy/pom.xml index 0248323c497..09e637e1702 100644 --- a/archetypes/legacy/pom.xml +++ b/archetypes/legacy/pom.xml @@ -23,7 +23,7 @@ io.helidon.archetypes helidon-archetypes-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-archetypes-legacy-project Helidon Legacy Archetypes diff --git a/archetypes/legacy/quickstart-mp/pom.xml b/archetypes/legacy/quickstart-mp/pom.xml index f0b936918a4..e4168bd6517 100644 --- a/archetypes/legacy/quickstart-mp/pom.xml +++ b/archetypes/legacy/quickstart-mp/pom.xml @@ -23,7 +23,7 @@ io.helidon.archetypes helidon-archetypes-legacy-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-quickstart-mp helidon-archetype diff --git a/archetypes/legacy/quickstart-se/pom.xml b/archetypes/legacy/quickstart-se/pom.xml index 8c7fe0d7398..8dcbf902502 100644 --- a/archetypes/legacy/quickstart-se/pom.xml +++ b/archetypes/legacy/quickstart-se/pom.xml @@ -23,7 +23,7 @@ io.helidon.archetypes helidon-archetypes-legacy-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-quickstart-se helidon-archetype diff --git a/archetypes/pom.xml b/archetypes/pom.xml index 1006c562d8a..b840db697bc 100644 --- a/archetypes/pom.xml +++ b/archetypes/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.archetypes helidon-archetypes-project diff --git a/bom/pom.xml b/bom/pom.xml index d6b6d76825a..8b6af76b14d 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-parent - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../parent/pom.xml helidon-bom @@ -31,7 +31,7 @@ Helidon BOM POM - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT @@ -610,6 +610,11 @@ helidon-common-features ${helidon.version} + + io.helidon.common.concurrency + helidon-common-concurrency-limits + ${helidon.version} + @@ -781,6 +786,16 @@ helidon-lra-coordinator-narayana-client ${helidon.version} + + io.helidon.lra + helidon-lra-coordinator-server + ${helidon.version} + + + io.helidon.microprofile.lra + helidon-microprofile-lra-testing + ${helidon.version} + @@ -1303,6 +1318,11 @@ helidon-webserver-service-common ${helidon.version} + + io.helidon.webserver + helidon-webserver-concurrency-limits + ${helidon.version} + io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5 diff --git a/builder/api/pom.xml b/builder/api/pom.xml index b10f24bb656..7e6a6158597 100644 --- a/builder/api/pom.xml +++ b/builder/api/pom.xml @@ -24,7 +24,7 @@ io.helidon.builder helidon-builder-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/codegen/pom.xml b/builder/codegen/pom.xml index a196148d27d..164695c1b40 100644 --- a/builder/codegen/pom.xml +++ b/builder/codegen/pom.xml @@ -23,7 +23,7 @@ io.helidon.builder helidon-builder-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java index a7ad18404ff..2336ba4426e 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java @@ -255,7 +255,7 @@ private void process(RoundContext roundContext, TypeInfo blueprint) { roundContext.addGeneratedType(prototype, classModel, blueprint.typeName(), - blueprint.originatingElement().orElse(blueprint.typeName())); + blueprint.originatingElementValue()); if (typeContext.typeInfo().supportsServiceRegistry() && typeContext.propertyData().hasProvider()) { for (PrototypeProperty property : typeContext.propertyData().properties()) { diff --git a/builder/pom.xml b/builder/pom.xml index 799ea2c7eea..6362a060faa 100644 --- a/builder/pom.xml +++ b/builder/pom.xml @@ -25,7 +25,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/processor/pom.xml b/builder/processor/pom.xml index 1e7b5629d1a..9bfc0949cc1 100644 --- a/builder/processor/pom.xml +++ b/builder/processor/pom.xml @@ -23,7 +23,7 @@ io.helidon.builder helidon-builder-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/tests/builder/pom.xml b/builder/tests/builder/pom.xml index 1f8179ba8cd..e60957cd550 100644 --- a/builder/tests/builder/pom.xml +++ b/builder/tests/builder/pom.xml @@ -23,7 +23,7 @@ io.helidon.builder.tests helidon-builder-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/tests/codegen/pom.xml b/builder/tests/codegen/pom.xml index 6ffb4e0b0f8..56aae2c3df5 100644 --- a/builder/tests/codegen/pom.xml +++ b/builder/tests/codegen/pom.xml @@ -23,7 +23,7 @@ io.helidon.builder.tests helidon-builder-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/tests/common-types/pom.xml b/builder/tests/common-types/pom.xml index 5e4303ce1ea..f5613f14b5b 100644 --- a/builder/tests/common-types/pom.xml +++ b/builder/tests/common-types/pom.xml @@ -23,7 +23,7 @@ io.helidon.builder.tests helidon-builder-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java index 07f13ac5c29..3aab5620bdf 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java @@ -43,6 +43,8 @@ public interface Annotated { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @return list of all meta annotations of this element */ @@ -85,11 +87,10 @@ default Optional findAnnotation(TypeName annotationType) { * @see #findAnnotation(TypeName) */ default Annotation annotation(TypeName annotationType) { - return findAnnotation(annotationType).orElseThrow(() -> new NoSuchElementException("Annotation " + annotationType + " " - + "is not present. Guard " - + "with hasAnnotation(), or " - + "use findAnnotation() " - + "instead")); + return findAnnotation(annotationType) + .orElseThrow(() -> new NoSuchElementException("Annotation " + annotationType + " is not present. " + + "Guard with hasAnnotation(), " + + "or use findAnnotation() instead")); } /** diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java index 3017fdd6526..b51fb659b6d 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java @@ -79,6 +79,15 @@ interface AnnotationBlueprint { @Option.Singular Map values(); + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @return list of all annotations declared on the annotation type, or inherited from them + */ + @Option.Redundant + @Option.Singular + List metaAnnotations(); + /** * The value property. * @@ -653,4 +662,19 @@ default > Optional> enumValues(String property, Class< return AnnotationSupport.asEnums(typeName(), values(), property, type); } + /** + * Check if {@link io.helidon.common.types.Annotation#metaAnnotations()} contains an annotation of the provided type. + *

+ * Note: we ignore {@link java.lang.annotation.Target}, {@link java.lang.annotation.Inherited}, + * {@link java.lang.annotation.Documented}, and {@link java.lang.annotation.Retention}. + * + * @param annotationType type of annotation + * @return {@code true} if the annotation is declared on this annotation, or is inherited from a declared annotation + */ + default boolean hasMetaAnnotation(TypeName annotationType) { + return metaAnnotations() + .stream() + .map(Annotation::typeName) + .anyMatch(annotationType::equals); + } } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java index 52501d9d95e..b21a8a1a917 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java @@ -532,6 +532,14 @@ private static String asString(TypeName typeName, String property, Object value) return str; } + if (value instanceof TypeName tn) { + return tn.fqName(); + } + + if (value instanceof EnumValue ev) { + return ev.name(); + } + if (value instanceof List) { throw new IllegalArgumentException(typeName.fqName() + " property " + property + " is a list, cannot be converted to String"); @@ -619,22 +627,30 @@ private static Class asClass(TypeName typeName, String property, Object value if (value instanceof Class theClass) { return theClass; } - if (value instanceof String str) { - try { - return Class.forName(str); - } catch (ClassNotFoundException e) { + + String className = switch (value) { + case TypeName tn -> tn.name(); + case String str -> str; + default -> { throw new IllegalArgumentException(typeName.fqName() + " property " + property - + " of type String and value \"" + str + "\"" + + " of type " + value.getClass().getName() + " cannot be converted to Class"); } - } + }; - throw new IllegalArgumentException(typeName.fqName() + " property " + property - + " of type " + value.getClass().getName() - + " cannot be converted to Class"); + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(typeName.fqName() + " property " + property + + " of type String and value \"" + className + "\"" + + " cannot be converted to Class"); + } } private static TypeName asTypeName(TypeName typeName, String property, Object value) { + if (value instanceof TypeName tn) { + return tn; + } if (value instanceof Class theClass) { return TypeName.create(theClass); } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java new file mode 100644 index 00000000000..a167313516d --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.List; + +/** + * Signature of a {@link io.helidon.common.types.TypedElementInfo}. + *

+ * The {@link io.helidon.common.types.TypedElementInfo#signature()} is intended to compare + * fields, methods, and constructors across type hierarchy - for example when looking for a method + * that we override. + *

+ * The following information is used for equals and hash-code: + *

+ * + * The signature has well-defined {@code hashCode} and {@code equals} methods, + * so it can be safely used as a key in a {@link java.util.Map}. + *

+ * This interface is sealed, an instance can only be obtained + * from {@link io.helidon.common.types.TypedElementInfo#signature()}. + * + * @see #text() + */ +public sealed interface ElementSignature permits ElementSignatures.FieldSignature, + ElementSignatures.MethodSignature, + ElementSignatures.ParameterSignature, + ElementSignatures.NoSignature { + /** + * Type of the element. Resolves as follows: + *

+ * + * @return type of this element, never used for equals or hashCode + */ + TypeName type(); + + /** + * Name of the element. For constructor, this always returns {@code }, + * for parameters, this method may return the real parameter name or an index + * parameter name depending on the source of the information (during annotation processing, + * this would be the actual parameter name, when classpath scanning, this would be something like + * {@code param0}. + * + * @return name of this element + */ + String name(); + + /** + * Types of parameters if this represents a method or a constructor, + * empty {@link java.util.List} otherwise. + * + * @return parameter types + */ + List parameterTypes(); + + /** + * A text representation of this signature. + * + *
    + *
  • Field: field name (such as {@code myNiceField}
  • + *
  • Constructor: comma separated parameter types (no generics) in parentheses (such as + * {@code (java.lang.String,java.util.List)})
  • + *
  • Method: method name, parameter types (no generics) in parentheses (such as + * {@code methodName(java.lang.String,java.util.List)}
  • + *
  • Parameter: parameter name (such as {@code myParameter} or {@code param0} - not very useful, as parameter names + * are not carried over to compiled code in Java
  • + *
+ * + * @return text representation + */ + String text(); +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignatures.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignatures.java new file mode 100644 index 00000000000..4f5be563f29 --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignatures.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +final class ElementSignatures { + private ElementSignatures() { + } + + static ElementSignature createNone() { + return new NoSignature(); + } + + static ElementSignature createField(TypeName type, + String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new FieldSignature(type, name); + } + + static ElementSignature createConstructor(List parameters) { + Objects.requireNonNull(parameters); + return new MethodSignature(TypeNames.PRIMITIVE_VOID, + "", + parameters); + } + + static ElementSignature createMethod(TypeName returnType, String name, List parameters) { + Objects.requireNonNull(returnType); + Objects.requireNonNull(name); + Objects.requireNonNull(parameters); + return new MethodSignature(returnType, + name, + parameters); + } + + static ElementSignature createParameter(TypeName type, String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new ParameterSignature(type, name); + } + + static final class FieldSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private FieldSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FieldSignature that)) { + return false; + } + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class MethodSignature implements ElementSignature { + private final TypeName type; + private final String name; + private final List parameters; + private final String text; + private final boolean constructor; + + private MethodSignature(TypeName type, + String name, + List parameters) { + this.type = type; + this.name = name; + this.parameters = parameters; + if (name.equals("")) { + this.constructor = true; + this.text = parameterTypesSection(parameters, ",", TypeName::fqName); + } else { + this.constructor = false; + this.text = name + parameterTypesSection(parameters, ",", TypeName::fqName); + } + + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return parameters; + } + + @Override + public String text() { + return text; + } + + @Override + public String toString() { + if (constructor) { + return text; + } else { + return type.resolvedName() + " " + name + parameterTypesSection(parameters, + ", ", + TypeName::resolvedName); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MethodSignature that)) { + return false; + } + return Objects.equals(name, that.name) && Objects.equals(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(name, parameters); + } + } + + static final class ParameterSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private ParameterSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ParameterSignature that)) { + return false; + } + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class NoSignature implements ElementSignature { + @Override + public TypeName type() { + return TypeNames.PRIMITIVE_VOID; + } + + @Override + public String name() { + return ""; + } + + @Override + public String text() { + return ""; + } + + @Override + public String toString() { + return text(); + } + + @Override + public List parameterTypes() { + return List.of(); + } + } + + private static String parameterTypesSection(List parameters, + String delimiter, + Function typeMapper) { + return parameters.stream() + .map(typeMapper) + .collect(Collectors.joining(delimiter, "(", ")")); + } +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValue.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValue.java index 34fa57b6990..3a405d354f2 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValue.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValue.java @@ -16,6 +16,8 @@ package io.helidon.common.types; +import java.util.Objects; + /** * When creating an {@link io.helidon.common.types.Annotation}, we may need to create an enum value * without access to the enumeration. @@ -31,17 +33,24 @@ public interface EnumValue { * @return enum value */ static EnumValue create(TypeName enumType, String enumName) { - return new EnumValue() { - @Override - public TypeName type() { - return enumType; - } + Objects.requireNonNull(enumType); + Objects.requireNonNull(enumName); + return new EnumValueImpl(enumType, enumName); + } + + /** + * Create a new enum value. + * + * @param type enum type + * @param value enum value constant + * @return new enum value + * @param type of the enum + */ + static > EnumValue create(Class type, T value) { + Objects.requireNonNull(type); + Objects.requireNonNull(value); - @Override - public String name() { - return enumName; - } - }; + return new EnumValueImpl(TypeName.create(type), value.name()); } /** diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValueImpl.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValueImpl.java new file mode 100644 index 00000000000..f2205b2452e --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValueImpl.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.Objects; + +final class EnumValueImpl implements EnumValue { + private final TypeName type; + private final String name; + + EnumValueImpl(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof EnumValue enumValue)) { + return false; + } + return Objects.equals(type, enumValue.type()) && Objects.equals(name, enumValue.name()); + } + + @Override + public int hashCode() { + return Objects.hash(type, name); + } + + @Override + public String toString() { + return type.fqName() + "." + name; + } +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java index 87a1e75e4db..7d36ffc7ece 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,23 @@ public enum Modifier { /** * The {@code final} modifier. */ - FINAL("final"); + FINAL("final"), + /** + * The {@code transient} modifier. + */ + TRANSIENT("transient"), + /** + * The {@code volatile} modifier. + */ + VOLATILE("volatile"), + /** + * The {@code synchronized} modifier. + */ + SYNCHRONIZED("synchronized"), + /** + * The {@code native} modifier. + */ + NATIVE("native"); private final String modifierName; diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java index 4bb3da96300..704c34d139c 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java @@ -16,10 +16,13 @@ package io.helidon.common.types; +import java.util.ArrayDeque; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Queue; import java.util.Set; import io.helidon.builder.api.Option; @@ -228,6 +231,54 @@ default Optional metaAnnotation(TypeName annotation, TypeName metaAn @Option.Redundant Optional originatingElement(); + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypeInfo#typeName()} if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code ClassInfo} when using classpath scanning. + * + * @return originating element, or the type of this type info + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::typeName); + } + + /** + * Checks if the current type implements, or extends the provided type. + * This method analyzes the whole dependency tree of the current type. + * + * @param typeName type of interface to check + * @return the super type info, or interface type info matching the provided type, with appropriate generic declarations + */ + default Optional findInHierarchy(TypeName typeName) { + if (typeName.equals(typeName())) { + return Optional.of((TypeInfo) this); + } + // scan super types + Optional superClass = superTypeInfo(); + if (superClass.isPresent() && !superClass.get().typeName().equals(TypeNames.OBJECT)) { + var superType = superClass.get(); + var foundInSuper = superType.findInHierarchy(typeName); + if (foundInSuper.isPresent()) { + return foundInSuper; + } + } + // nope, let's try interfaces + Queue interfaces = new ArrayDeque<>(interfaceTypeInfo()); + Set processed = new HashSet<>(); + + while (!interfaces.isEmpty()) { + TypeInfo type = interfaces.remove(); + // make sure we process each type only once + if (processed.add(type.typeName())) { + if (typeName.equals(type.typeName())) { + return Optional.of(type); + } + interfaces.addAll(type.interfaceTypeInfo()); + } + } + return Optional.empty(); + } + /** * Uses {@link io.helidon.common.types.TypeInfo#referencedModuleNames()} to determine if the module name is known for the * given type. diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java index 62fe59da5a3..f539e5849af 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java @@ -16,8 +16,10 @@ package io.helidon.common.types; +import java.lang.annotation.Documented; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.time.Duration; import java.util.Collection; import java.util.List; @@ -73,10 +75,18 @@ public final class TypeNames { * Type name for {@link java.lang.annotation.Retention}. */ public static final TypeName RETENTION = TypeName.create(Retention.class); + /** + * Type name for {@link java.lang.annotation.Documented}. + */ + public static final TypeName DOCUMENTED = TypeName.create(Documented.class); /** * Type name for {@link java.lang.annotation.Inherited}. */ public static final TypeName INHERITED = TypeName.create(Inherited.class); + /** + * Type name for {@link java.lang.annotation.Target}. + */ + public static final TypeName TARGET = TypeName.create(Target.class); /* Primitive types and their boxed counterparts diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java index b52ba88a4bd..3de312eb13c 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java @@ -167,4 +167,25 @@ interface TypedElementInfoBlueprint extends Annotated { */ @Option.Redundant Optional originatingElement(); + + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypedElementInfo#signature()} + * if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code MethodInfo} (and such) when using classpath scanning. + * + * @return originating element, or the signature of this element + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::signature); + } + + /** + * Signature of this element. + * + * @return signature of this element + * @see io.helidon.common.types.ElementSignature + */ + @Option.Access("") + ElementSignature signature(); } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java index 9edf23abd11..0cba7edaff8 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.helidon.common.types; +import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; @@ -60,10 +61,67 @@ static class BuilderDecorator implements Prototype.BuilderDecorator target) { -/* + backwardCompatibility(target); + constructorName(target); + signature(target); + } + + private void signature(TypedElementInfo.BuilderBase target) { + if (target.kind().isEmpty()) { + // this will fail when validating + target.signature(ElementSignatures.createNone()); + return; + } else { + target.signature(signature(target, target.kind().get())); + } + } + + private ElementSignature signature(TypedElementInfo.BuilderBase target, ElementKind elementKind) { + if (elementKind == ElementKind.CONSTRUCTOR) { + return ElementSignatures.createConstructor(toTypes(target.parameterArguments())); + } + // for everything else we need the type (it is required) + if (target.typeName().isEmpty() || target.elementName().isEmpty()) { + return ElementSignatures.createNone(); + } + + TypeName typeName = target.typeName().get(); + String name = target.elementName().get(); + + if (elementKind == ElementKind.FIELD + || elementKind == ElementKind.RECORD_COMPONENT + || elementKind == ElementKind.ENUM_CONSTANT) { + return ElementSignatures.createField(typeName, name); + } + if (elementKind == ElementKind.METHOD) { + return ElementSignatures.createMethod(typeName, name, toTypes(target.parameterArguments())); + } + if (elementKind == ElementKind.PARAMETER) { + return ElementSignatures.createParameter(typeName, name); + } + return ElementSignatures.createNone(); + } + + private List toTypes(List typedElementInfos) { + return typedElementInfos.stream() + .map(TypedElementInfo::typeName) + .collect(Collectors.toUnmodifiableList()); + } + + private void constructorName(TypedElementInfo.BuilderBase target) { + Optional elementKind = target.kind(); + if (elementKind.isPresent()) { + if (elementKind.get() == ElementKind.CONSTRUCTOR) { + target.elementName(""); + } + } + } + + @SuppressWarnings("removal") + private void backwardCompatibility(TypedElementInfo.BuilderBase target) { + /* Backward compatibility for deprecated methods. */ if (target.kind().isEmpty() && target.elementTypeKind().isPresent()) { @@ -103,14 +161,6 @@ public void decorate(TypedElementInfo.BuilderBase target) { target.addModifier(typeModifier.modifierName()); } target.addModifier(target.accessModifier().get().modifierName()); - - - Optional elementKind = target.kind(); - if (elementKind.isPresent()) { - if (elementKind.get() == ElementKind.CONSTRUCTOR) { - target.elementName(""); - } - } } } } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/package-info.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/package-info.java new file mode 100644 index 00000000000..7851acd401b --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Subset of Builder's SPI types that are useful for runtime. Used in the ConfigBean builder, etc., that require a minimal set of + * types present at runtime. + */ +package io.helidon.common.types; diff --git a/builder/tests/pom.xml b/builder/tests/pom.xml index 50a4e39f973..094e16c96ff 100644 --- a/builder/tests/pom.xml +++ b/builder/tests/pom.xml @@ -25,7 +25,7 @@ io.helidon.builder helidon-builder-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/bundles/config/pom.xml b/bundles/config/pom.xml index 78f110fa2df..65e32af242e 100644 --- a/bundles/config/pom.xml +++ b/bundles/config/pom.xml @@ -22,7 +22,7 @@ io.helidon.bundles helidon-bundles-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-bundles-config diff --git a/bundles/pom.xml b/bundles/pom.xml index 048f8e7a97b..4e005cbda81 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT config diff --git a/bundles/security/pom.xml b/bundles/security/pom.xml index 8cffd66ef99..4eb18835863 100644 --- a/bundles/security/pom.xml +++ b/bundles/security/pom.xml @@ -22,7 +22,7 @@ io.helidon.bundles helidon-bundles-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-bundles-security diff --git a/codegen/apt/pom.xml b/codegen/apt/pom.xml index f8e916dbdb4..c66fa84db02 100644 --- a/codegen/apt/pom.xml +++ b/codegen/apt/pom.xml @@ -22,7 +22,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-codegen-apt diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java index 45d998f2bc6..5f080864e79 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java @@ -16,8 +16,11 @@ package io.helidon.codegen.apt; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; +import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; @@ -26,6 +29,7 @@ import io.helidon.common.types.Annotation; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; /** * Factory for annotations. @@ -48,7 +52,43 @@ public static Annotation createAnnotation(AnnotationMirror am, .orElseThrow(() -> new IllegalArgumentException("Cannot create annotation for non-existent type: " + am.getAnnotationType())); - return Annotation.create(val, extractAnnotationValues(am, elements)); + // ignore these annotations, unless one of them was explicitly requested + var set = new HashSet(); + set.add(TypeNames.INHERITED); + set.add(TypeNames.TARGET); + set.add(TypeNames.RETENTION); + set.add(TypeNames.DOCUMENTED); + set.remove(val); + + return createAnnotation(elements, am, set) + .orElseThrow(); + } + + private static Optional createAnnotation(Elements elements, AnnotationMirror am, Set processedTypes) { + TypeName val = AptTypeFactory.createTypeName(am.getAnnotationType()) + .orElseThrow(() -> new IllegalArgumentException("Cannot create annotation for non-existent type: " + + am.getAnnotationType())); + + if (processedTypes.contains(val)) { + return Optional.empty(); + } + + Annotation.Builder builder = Annotation.builder(); + + elements.getAllAnnotationMirrors(am.getAnnotationType().asElement()) + .stream() + .map(it -> { + var newProcessed = new HashSet<>(processedTypes); + newProcessed.add(val); + return createAnnotation(elements, it, newProcessed); + }) + .flatMap(Optional::stream) + .forEach(builder::addMetaAnnotation); + + return Optional.of(builder + .typeName(val) + .values(extractAnnotationValues(am, elements)) + .build()); } /** diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java index df4cc10db60..9efa87e8e72 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java @@ -40,6 +40,7 @@ import io.helidon.codegen.FilerTextResource; import io.helidon.codegen.IndentType; import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.common.types.TypeName; import static java.nio.charset.StandardCharsets.UTF_8; @@ -73,6 +74,23 @@ public Path writeSourceFile(ClassModel classModel, Object... originatingElements } } + @Override + public Path writeSourceFile(TypeName type, String content, Object... originatingElements) { + Element[] elements = toElements(originatingElements); + + try { + JavaFileObject sourceFile = filer.createSourceFile(type.fqName(), elements); + try (Writer os = sourceFile.openWriter()) { + os.write(content); + } + return Path.of(sourceFile.toUri()); + } catch (IOException e) { + throw new CodegenException("Failed to write source file for type: " + type, + e, + originatingElement(elements, type)); + } + } + @Override public Path writeResource(byte[] resource, String location, Object... originatingElements) { Element[] elements = toElements(originatingElements); diff --git a/codegen/class-model/pom.xml b/codegen/class-model/pom.xml index 9c4eb7a3bdd..60bb27f93fc 100644 --- a/codegen/class-model/pom.xml +++ b/codegen/class-model/pom.xml @@ -22,7 +22,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-codegen-class-model diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java index d5055caae68..26dc36a78fa 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java @@ -106,7 +106,7 @@ static void addCreateElement(ContentBuilder contentBuilder, TypedElementInfo static void addCreateAnnotation(ContentBuilder contentBuilder, Annotation annotation) { Map values = annotation.values(); - if (values.isEmpty()) { + if (values.isEmpty() && annotation.metaAnnotations().isEmpty()) { // Annotation.create(TypeName.create("my.type.AnnotationType")) contentBuilder.addContent(ANNOTATION) .addContent(".create(") @@ -136,6 +136,16 @@ static void addCreateAnnotation(ContentBuilder contentBuilder, Annotation ann contentBuilder.addContentLine(")"); }); + // .addMetaAnnotation(...) + annotation.metaAnnotations() + .forEach(it -> contentBuilder.addContent(".addMetaAnnotation(") + .increaseContentPadding() + .increaseContentPadding() + .addContentCreate(it) + .addContentLine(")") + .decreaseContentPadding() + .decreaseContentPadding()); + // .build() contentBuilder.addContentLine(".build()") .decreaseContentPadding() diff --git a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java index 85aea878fc1..0e95c163447 100644 --- a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java +++ b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java @@ -57,6 +57,37 @@ void testPrintEnumValue() { private String name;""")); } + @Test + void testMetaAnnotation() { + Field field = Field.builder() + .accessModifier(AccessModifier.PRIVATE) + .type(Annotation.class) + .name("annotation") + .addContentCreate(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("value", "someValue") + .addMetaAnnotation(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("value", "string") + .build()) + .build()) + .build(); + String text = write(field); + + String expected = """ + private Annotation annotation = Annotation.builder() + .typeName(TypeName.create("org.junit.jupiter.api.Test")) + .putValue("value", "someValue") + .addMetaAnnotation(Annotation.builder() + .typeName(TypeName.create("org.junit.jupiter.api.Test")) + .putValue("value", "string") + .build() + ) + .build();"""; + + assertThat(text, is(expected)); + } + @Test void testContentCreateEnumValue() { TypeName enumType = TypeName.create(TestEnum.class); diff --git a/codegen/codegen/pom.xml b/codegen/codegen/pom.xml index bdff1303d4a..1d7218a6904 100644 --- a/codegen/codegen/pom.xml +++ b/codegen/codegen/pom.xml @@ -23,7 +23,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-codegen diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java index d8381653f8f..abd03ec7392 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java @@ -37,11 +37,21 @@ public interface CodegenFiler { * * @param classModel class model to write out * @param originatingElements elements that caused this type to be generated - * (you can use {@link io.helidon.common.types.TypeInfo#originatingElement()} for example + * (you can use {@link io.helidon.common.types.TypeInfo#originatingElementValue()}) * @return written path, we expect to always run on local file system */ Path writeSourceFile(ClassModel classModel, Object... originatingElements); + /** + * Write a source file using string content. + * + * @param type type of the file to generate + * @param content source code to write + * @param originatingElements elements that caused this type to be generated + * @return written path, we expect to always run on local file system + */ + Path writeSourceFile(TypeName type, String content, Object... originatingElements); + /** * Write a resource file. * diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenValidator.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenValidator.java index ff3d21ebb2d..97b61cc9da1 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenValidator.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenValidator.java @@ -53,8 +53,7 @@ public static String validateUri(TypeName enclosingType, + property + "(): " + "\"" + value + "\" cannot be parsed. Invalid URI.", e, - element.originatingElement().orElseGet(() -> enclosingType.fqName() + "." - + element.elementName())); + element.originatingElementValue()); } } @@ -84,8 +83,7 @@ public static String validateDuration(TypeName enclosingType, + " expression such as 'PT1S' (1 second), 'PT0.1S' (tenth of a second)." + " Please check javadoc of " + Duration.class.getName() + " class.", e, - element.originatingElement().orElseGet(() -> enclosingType.fqName() + "." - + element.elementName())); + element.originatingElementValue()); } } } diff --git a/codegen/compiler/pom.xml b/codegen/compiler/pom.xml index ad41538b9d4..ab015c4d3f2 100644 --- a/codegen/compiler/pom.xml +++ b/codegen/compiler/pom.xml @@ -22,7 +22,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-codegen-compiler diff --git a/codegen/helidon-copyright/pom.xml b/codegen/helidon-copyright/pom.xml index dba3e6afe92..66ae03ed0af 100644 --- a/codegen/helidon-copyright/pom.xml +++ b/codegen/helidon-copyright/pom.xml @@ -23,7 +23,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-codegen-helidon-copyright diff --git a/codegen/pom.xml b/codegen/pom.xml index 4790bf3c7af..b0671ab9845 100644 --- a/codegen/pom.xml +++ b/codegen/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.codegen diff --git a/codegen/scan/pom.xml b/codegen/scan/pom.xml index ad049c8418e..ed85545c6af 100644 --- a/codegen/scan/pom.xml +++ b/codegen/scan/pom.xml @@ -22,7 +22,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-codegen-scan diff --git a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java index 6861e4d6b2d..8d4c1738f24 100644 --- a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java +++ b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java @@ -18,19 +18,23 @@ import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import io.helidon.common.types.Annotation; import io.helidon.common.types.EnumValue; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; import io.github.classgraph.AnnotationClassRef; import io.github.classgraph.AnnotationEnumValue; import io.github.classgraph.AnnotationInfo; import io.github.classgraph.AnnotationParameterValue; import io.github.classgraph.AnnotationParameterValueList; +import io.github.classgraph.ClassInfo; /** * Factory for annotations. @@ -50,7 +54,44 @@ public static Annotation createAnnotation(ScanContext ctx, AnnotationInfo am) { TypeName typeName = ScanTypeFactory.create(am.getClassInfo()); - return Annotation.create(typeName, extractAnnotationValues(ctx, am)); + // ignore these annotations, unless one of them was explicitly requested + var set = new HashSet(); + set.add(TypeNames.INHERITED); + set.add(TypeNames.TARGET); + set.add(TypeNames.RETENTION); + set.add(TypeNames.DOCUMENTED); + set.remove(typeName); + + return createAnnotation(ctx, am, set) + .orElseThrow(); + } + + private static Optional createAnnotation(ScanContext ctx, AnnotationInfo am, HashSet processedTypes) { + ClassInfo classInfo = am.getClassInfo(); + if (classInfo == null) { + // cannot analyze this annotation + return Optional.empty(); + } + TypeName typeName = ScanTypeFactory.create(classInfo); + + if (processedTypes.contains(typeName)) { + return Optional.empty(); + } + var builder = Annotation.builder(); + + classInfo.getAnnotationInfo() + .stream() + .map(it -> { + var newProcessed = new HashSet<>(processedTypes); + newProcessed.add(typeName); + return createAnnotation(ctx, it, newProcessed); + }) + .flatMap(Optional::stream) + .forEach(builder::addMetaAnnotation); + + return Optional.of(builder.typeName(typeName) + .values(extractAnnotationValues(ctx, am)) + .build()); } /** diff --git a/codegen/tests/pom.xml b/codegen/tests/pom.xml index 8d6804e59b3..aeed442c09b 100644 --- a/codegen/tests/pom.xml +++ b/codegen/tests/pom.xml @@ -22,7 +22,7 @@ io.helidon.codegen helidon-codegen-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../pom.xml 4.0.0 diff --git a/codegen/tests/test-codegen-use/pom.xml b/codegen/tests/test-codegen-use/pom.xml index 1fafba1fa1f..180c0b70a5b 100644 --- a/codegen/tests/test-codegen-use/pom.xml +++ b/codegen/tests/test-codegen-use/pom.xml @@ -22,7 +22,7 @@ io.helidon.codegen.tests helidon-codegen-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../pom.xml 4.0.0 diff --git a/codegen/tests/test-codegen/pom.xml b/codegen/tests/test-codegen/pom.xml index d031df5af68..5e2d75151ef 100644 --- a/codegen/tests/test-codegen/pom.xml +++ b/codegen/tests/test-codegen/pom.xml @@ -22,7 +22,7 @@ io.helidon.codegen.tests helidon-codegen-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../pom.xml 4.0.0 diff --git a/common/buffers/pom.xml b/common/buffers/pom.xml index 8b6602c285e..cc5c6510bec 100644 --- a/common/buffers/pom.xml +++ b/common/buffers/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-buffers Helidon Common Buffers diff --git a/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java b/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java index f2402fb75c9..312169be55b 100644 --- a/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java +++ b/common/buffers/src/main/java/io/helidon/common/buffers/DataReader.java @@ -445,6 +445,9 @@ public int findNewLine(int max) throws IncorrectNewLineException { } indexWithinNode = crIndex + 1; idx += indexWithinNode; + if (idx >= max) { + return max; + } continue; } } diff --git a/common/buffers/src/test/java/io/helidon/common/buffers/DataReaderTest.java b/common/buffers/src/test/java/io/helidon/common/buffers/DataReaderTest.java new file mode 100644 index 00000000000..0d88178d1d2 --- /dev/null +++ b/common/buffers/src/test/java/io/helidon/common/buffers/DataReaderTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.buffers; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class DataReaderTest { + + @Test + void testFindNewLineWithLoneCR() { + // reading N bytes at a time until a new line is found + // with data containing a lone CR + + byte[] data = new byte[] {0, 0, (byte) '\r', 0, (byte) '\r', (byte) '\n'}; + AtomicReference ref = new AtomicReference<>(data); + DataReader dataReader = new DataReader(() -> ref.getAndSet(null), true); + + int n = 2; + assertThat(dataReader.findNewLine(n), is(n)); + dataReader.skip(n); + assertThat(dataReader.findNewLine(n), is(n)); + dataReader.skip(n); + assertThat(dataReader.findNewLine(n), is(0)); + } +} diff --git a/common/common/pom.xml b/common/common/pom.xml index 041db05b592..791e8756e85 100644 --- a/common/common/pom.xml +++ b/common/common/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common Helidon Common diff --git a/common/concurrency/limits/README.md b/common/concurrency/limits/README.md new file mode 100644 index 00000000000..c17184f10ff --- /dev/null +++ b/common/concurrency/limits/README.md @@ -0,0 +1,37 @@ +Concurrency Limits +----- + +This module provides concurrency limits, so we can limit the number of concurrent, in-progress operations (for example in WebServer). + +The implemented concurrency limits are: + +| Key | Weight | Description | +|---------|--------|--------------------------------------------------------------| +| `fixed` | `90` | Semaphore based concurrency limit, supports queueing | +| `aimd` | `80` | AIMD based limit (additive-increase/multiplicative-decrease) | + +Current usage: `helidon-webserver` + +The weight is not significant (unless you want to override an implementation using your own Limit with a higher weight), as the usages in Helidon use a single (optional) implementation that must be correctly typed in +configuration. + +# Fixed concurrency limit + +The fixed concurrency limit is based on a semaphore behavior. +You can define the number of available permits, then each time a token is requested, a permit (if available) is returned. +When the token is finished (through one of its lifecycle operations), the permit is returned. + +When the limit is set to 0, an unlimited implementation is used. + +The fixed limit also provides support for defining a queue. If set to a value above `0`, queuing is enabled. In such a case we enqueue a certain number of requests (with a configurable timeout). + +Defaults are: +- `permits: 0` - unlimited permits (no limit) +- `queue-length: 0` - no queuing +- `queue-timeout: PT1S` - 1 second timout in queue, if queuing is enabled + +# AIMD concurrency limit + +The additive-increase/multiplicative-decrease (AIMD) algorithm is a feedback control algorithm best known for its use in TCP congestion control. AIMD combines linear growth of the congestion window when there is no congestion with an exponential reduction when congestion is detected. + +This implementation provides variable concurrency limit with fixed minimal/maximal number of permits. diff --git a/common/concurrency/limits/pom.xml b/common/concurrency/limits/pom.xml new file mode 100644 index 00000000000..79f981da434 --- /dev/null +++ b/common/concurrency/limits/pom.xml @@ -0,0 +1,127 @@ + + + + + 4.0.0 + + io.helidon.common.concurrency + helidon-common-concurrency-project + 4.2.0-SNAPSHOT + ../pom.xml + + + helidon-common-concurrency-limits + Helidon Common Concurrency Limits + + + + io.helidon.common + helidon-common + + + io.helidon.builder + helidon-builder-api + + + io.helidon.common + helidon-common-config + + + io.helidon.service + helidon-service-registry + true + + + io.helidon.config + helidon-config + test + + + io.helidon.config + helidon-config-yaml + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimit.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimit.java new file mode 100644 index 00000000000..1c901e8b78c --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimit.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.Semaphore; +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.common.config.Config; + +/** + * AIMD based limiter. + *

+ * The additive-increase/multiplicative-decrease (AIMD) algorithm is a feedback control algorithm best known for its use in TCP + * congestion control. AIMD combines linear growth of the congestion window when there is no congestion with an exponential + * reduction when congestion is detected. + */ +@SuppressWarnings("removal") +@RuntimeType.PrototypedBy(AimdLimitConfig.class) +public class AimdLimit implements Limit, SemaphoreLimit, RuntimeType.Api { + static final String TYPE = "aimd"; + + private final AimdLimitConfig config; + private final AimdLimitImpl aimdLimitImpl; + + private AimdLimit(AimdLimitConfig config) { + this.config = config; + this.aimdLimitImpl = new AimdLimitImpl(config); + } + + /** + * Create a new fluent API builder to construct {@link io.helidon.common.concurrency.limits.AimdLimit} + * instance. + * + * @return fluent API builder + */ + public static AimdLimitConfig.Builder builder() { + return AimdLimitConfig.builder(); + } + + /** + * Create a new instance with all defaults. + * + * @return a new limit instance + */ + public static AimdLimit create() { + return builder().build(); + } + + /** + * Create a new instance from configuration. + * + * @param config configuration of the AIMD limit + * @return a new limit instance configured from {@code config} + */ + public static AimdLimit create(Config config) { + return builder() + .config(config) + .build(); + } + + /** + * Create a new instance from configuration. + * + * @param config configuration of the AIMD limit + * @return a new limit instance configured from {@code config} + */ + public static AimdLimit create(AimdLimitConfig config) { + return new AimdLimit(config); + } + + /** + * Create a new instance customizing its configuration. + * + * @param consumer consumer of configuration builder + * @return a new limit instance configured from the builder + */ + public static AimdLimit create(Consumer consumer) { + return builder() + .update(consumer) + .build(); + } + + @Override + public T invoke(Callable callable) throws Exception { + return aimdLimitImpl.invoke(callable); + } + + @Override + public void invoke(Runnable runnable) throws Exception { + aimdLimitImpl.invoke(runnable); + } + + @Override + public Optional tryAcquire(boolean wait) { + return aimdLimitImpl.tryAcquire(); + } + + @SuppressWarnings("removal") + @Override + public Semaphore semaphore() { + return aimdLimitImpl.semaphore(); + } + + @Override + public String name() { + return config.name(); + } + + @Override + public String type() { + return TYPE; + } + + @Override + public AimdLimitConfig prototype() { + return config; + } + + @Override + public Limit copy() { + return config.build(); + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitConfigBlueprint.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitConfigBlueprint.java new file mode 100644 index 00000000000..400fbb99682 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitConfigBlueprint.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.Optional; +import java.util.function.Supplier; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.concurrency.limits.spi.LimitProvider; + +/** + * Configuration of {@link io.helidon.common.concurrency.limits.AimdLimit}. + */ +@Prototype.Blueprint +@Prototype.Configured(value = AimdLimit.TYPE, root = false) +@Prototype.Provides(LimitProvider.class) +interface AimdLimitConfigBlueprint extends Prototype.Factory { + /** + * Backoff ratio to use for the algorithm. + * The value must be within [0.5, 1.0). + * + * @return backoff ratio + */ + @Option.Configured + @Option.DefaultDouble(0.9) + double backoffRatio(); + + /** + * Initial limit. + * The value must be within [{@link #minLimit()}, {@link #maxLimit()}]. + * + * @return initial limit + */ + @Option.Configured + @Option.DefaultInt(20) + int initialLimit(); + + /** + * Maximal limit. + * The value must be same or higher than {@link #minLimit()}. + * + * @return maximal limit + */ + @Option.Configured + @Option.DefaultInt(200) + int maxLimit(); + + /** + * Minimal limit. + * The value must be same or lower than {@link #maxLimit()}. + * + * @return minimal limit + */ + @Option.Configured + @Option.DefaultInt(20) + int minLimit(); + + /** + * Timeout that when exceeded is the same as if the task failed. + * + * @return task timeout, defaults to 5 seconds + */ + @Option.Configured + @Option.Default("PT5S") + Duration timeout(); + + /** + * A clock that supplies nanosecond time. + * + * @return supplier of current nanoseconds, defaults to {@link java.lang.System#nanoTime()} + */ + Optional> clock(); + + /** + * Name of this instance. + * + * @return name of the instance + */ + @Option.Default(AimdLimit.TYPE) + String name(); +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitImpl.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitImpl.java new file mode 100644 index 00000000000..8842965d5a1 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitImpl.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.io.Serial; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +import io.helidon.common.config.ConfigException; + +class AimdLimitImpl { + private final double backoffRatio; + private final long timeoutInNanos; + private final int minLimit; + private final int maxLimit; + + private final Supplier clock; + private final AtomicInteger concurrentRequests; + private final AdjustableSemaphore semaphore; + + private final AtomicInteger limit; + private final Lock limitLock = new ReentrantLock(); + + AimdLimitImpl(AimdLimitConfig config) { + int initialLimit = config.initialLimit(); + this.backoffRatio = config.backoffRatio(); + this.timeoutInNanos = config.timeout().toNanos(); + this.minLimit = config.minLimit(); + this.maxLimit = config.maxLimit(); + this.clock = config.clock().orElseGet(() -> System::nanoTime); + + this.concurrentRequests = new AtomicInteger(); + this.semaphore = new AdjustableSemaphore(initialLimit); + + this.limit = new AtomicInteger(initialLimit); + + if (!(backoffRatio < 1.0 && backoffRatio >= 0.5)) { + throw new ConfigException("Backoff ratio must be within [0.5, 1.0)"); + } + if (maxLimit < minLimit) { + throw new ConfigException("Max limit must be higher than min limit, or equal to it"); + } + if (initialLimit > maxLimit) { + throw new ConfigException("Initial limit must be lower than max limit, or equal to it"); + } + if (initialLimit < minLimit) { + throw new ConfigException("Initial limit must be higher than minimum limit, or equal to it"); + } + } + + Semaphore semaphore() { + return semaphore; + } + + int currentLimit() { + return limit.get(); + } + + Optional tryAcquire() { + if (!semaphore.tryAcquire()) { + return Optional.empty(); + } + + return Optional.of(new AimdToken(clock, concurrentRequests)); + } + + void invoke(Runnable runnable) throws Exception { + invoke(() -> { + runnable.run(); + return null; + }); + } + + T invoke(Callable callable) throws Exception { + long startTime = clock.get(); + int currentRequests = concurrentRequests.incrementAndGet(); + + if (semaphore.tryAcquire()) { + try { + T response = callable.call(); + updateWithSample(startTime, clock.get(), currentRequests, true); + return response; + } catch (IgnoreTaskException e) { + return e.handle(); + } catch (Throwable e) { + updateWithSample(startTime, clock.get(), currentRequests, false); + throw e; + } finally { + concurrentRequests.decrementAndGet(); + semaphore.release(); + } + } else { + throw new LimitException("No more permits available for the semaphore"); + } + } + + void updateWithSample(long startTime, long endTime, int currentRequests, boolean success) { + long rtt = endTime - startTime; + + int currentLimit = limit.get(); + if (rtt > timeoutInNanos || !success) { + currentLimit = (int) (currentLimit * backoffRatio); + } else if (currentRequests * 2 >= currentLimit) { + currentLimit = currentLimit + 1; + } + setLimit(Math.min(maxLimit, Math.max(minLimit, currentLimit))); + } + + private void setLimit(int newLimit) { + if (newLimit == limit.get()) { + // already have the correct limit + return; + } + // now we lock, to do this only once in parallel, + // as otherwise we may end up in strange lands + limitLock.lock(); + try { + int oldLimit = limit.get(); + if (oldLimit == newLimit) { + // parallel thread already fixed it + return; + } + limit.set(newLimit); + + if (newLimit > oldLimit) { + this.semaphore.release(newLimit - oldLimit); + } else { + this.semaphore.reducePermits(oldLimit - newLimit); + } + } finally { + limitLock.unlock(); + } + } + + private static final class AdjustableSemaphore extends Semaphore { + @Serial + private static final long serialVersionUID = 114L; + + private AdjustableSemaphore(int permits) { + super(permits); + } + + @Override + protected void reducePermits(int reduction) { + super.reducePermits(reduction); + } + } + + private class AimdToken implements Limit.Token { + private final long startTime; + private final int currentRequests; + + private AimdToken(Supplier clock, AtomicInteger concurrentRequests) { + startTime = clock.get(); + currentRequests = concurrentRequests.incrementAndGet(); + } + + @Override + public void dropped() { + updateWithSample(startTime, clock.get(), currentRequests, false); + } + + @Override + public void ignore() { + concurrentRequests.decrementAndGet(); + } + + @Override + public void success() { + updateWithSample(startTime, clock.get(), currentRequests, true); + concurrentRequests.decrementAndGet(); + } + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitProvider.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitProvider.java new file mode 100644 index 00000000000..ae0af21e0ab --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/AimdLimitProvider.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import io.helidon.common.Weight; +import io.helidon.common.concurrency.limits.spi.LimitProvider; +import io.helidon.common.config.Config; + +/** + * {@link java.util.ServiceLoader} service provider for {@link io.helidon.common.concurrency.limits.AimdLimit} + * limit implementation. + */ +@Weight(80) +public class AimdLimitProvider implements LimitProvider { + /** + * Constructor required by the service loader. + */ + public AimdLimitProvider() { + } + + @Override + public String configKey() { + return AimdLimit.TYPE; + } + + @Override + public Limit create(Config config, String name) { + return AimdLimit.builder() + .config(config) + .name(name) + .build(); + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimit.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimit.java new file mode 100644 index 00000000000..97255156e4e --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimit.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.common.config.Config; + +/** + * Semaphore based limit, that supports queuing for a permit, and timeout on the queue. + * The default behavior is non-queuing. + * + * @see io.helidon.common.concurrency.limits.FixedLimitConfig + */ +@SuppressWarnings("removal") +@RuntimeType.PrototypedBy(FixedLimitConfig.class) +public class FixedLimit implements Limit, SemaphoreLimit, RuntimeType.Api { + /** + * Default limit, meaning unlimited execution. + */ + public static final int DEFAULT_LIMIT = 0; + /** + * Default length of the queue. + */ + public static final int DEFAULT_QUEUE_LENGTH = 0; + /** + * Timeout of a request that is enqueued. + */ + public static final String DEFAULT_QUEUE_TIMEOUT_DURATION = "PT1S"; + + static final String TYPE = "fixed"; + + private final FixedLimitConfig config; + private final LimiterHandler handler; + private final int initialPermits; + + private FixedLimit(FixedLimitConfig config) { + this.config = config; + if (config.permits() == 0 && config.semaphore().isEmpty()) { + this.handler = new NoOpSemaphoreHandler(); + this.initialPermits = 0; + } else { + Semaphore semaphore = config.semaphore().orElseGet(() -> new Semaphore(config.permits(), config.fair())); + this.initialPermits = semaphore.availablePermits(); + if (config.queueLength() == 0) { + this.handler = new RealSemaphoreHandler(semaphore); + } else { + this.handler = new QueuedSemaphoreHandler(semaphore, + config.queueLength(), + config.queueTimeout()); + } + } + } + + /** + * Create a new fluent API builder to construct {@link FixedLimit} + * instance. + * + * @return fluent API builder + */ + public static FixedLimitConfig.Builder builder() { + return FixedLimitConfig.builder(); + } + + /** + * Create a new instance with all defaults (no limit). + * + * @return a new limit instance + */ + public static FixedLimit create() { + return builder().build(); + } + + /** + * Create an instance from the provided semaphore. + * + * @param semaphore semaphore to use + * @return a new fixed limit backed by the provided semaphore + */ + public static FixedLimit create(Semaphore semaphore) { + return builder() + .semaphore(semaphore) + .build(); + } + + /** + * Create a new instance from configuration. + * + * @param config configuration of the fixed limit + * @return a new limit instance configured from {@code config} + */ + public static FixedLimit create(Config config) { + return builder() + .config(config) + .build(); + } + + /** + * Create a new instance from configuration. + * + * @param config configuration of the fixed limit + * @return a new limit instance configured from {@code config} + */ + public static FixedLimit create(FixedLimitConfig config) { + return new FixedLimit(config); + } + + /** + * Create a new instance customizing its configuration. + * + * @param consumer consumer of configuration builder + * @return a new limit instance configured from the builder + */ + public static FixedLimit create(Consumer consumer) { + return builder() + .update(consumer) + .build(); + } + + @Override + public T invoke(Callable callable) throws Exception { + return handler.invoke(callable); + } + + @Override + public void invoke(Runnable runnable) throws Exception { + handler.invoke(runnable); + } + + @Override + public Optional tryAcquire(boolean wait) { + return handler.tryAcquire(wait); + } + + @SuppressWarnings("removal") + @Override + public Semaphore semaphore() { + return handler.semaphore(); + } + + @Override + public FixedLimitConfig prototype() { + return config; + } + + @Override + public String name() { + return config.name(); + } + + @Override + public String type() { + return FixedLimit.TYPE; + } + + @Override + public Limit copy() { + if (config.semaphore().isPresent()) { + Semaphore semaphore = config.semaphore().get(); + + return FixedLimitConfig.builder() + .from(config) + .semaphore(new Semaphore(initialPermits, semaphore.isFair())) + .build(); + } + return config.build(); + } + + @SuppressWarnings("removal") + private interface LimiterHandler extends SemaphoreLimit, LimitAlgorithm { + } + + private static class NoOpSemaphoreHandler implements LimiterHandler { + private static final Token TOKEN = new Token() { + @Override + public void dropped() { + } + + @Override + public void ignore() { + } + + @Override + public void success() { + } + }; + + @Override + public T invoke(Callable callable) throws Exception { + try { + return callable.call(); + } catch (IgnoreTaskException e) { + return e.handle(); + } + } + + @Override + public void invoke(Runnable runnable) { + runnable.run(); + } + + @Override + public Optional tryAcquire(boolean wait) { + return Optional.of(TOKEN); + } + + @SuppressWarnings("removal") + @Override + public Semaphore semaphore() { + return NoopSemaphore.INSTANCE; + } + } + + @SuppressWarnings("removal") + private static class RealSemaphoreHandler implements LimiterHandler { + private final Semaphore semaphore; + + private RealSemaphoreHandler(Semaphore semaphore) { + this.semaphore = semaphore; + } + + @Override + public T invoke(Callable callable) throws Exception { + if (semaphore.tryAcquire()) { + try { + return callable.call(); + } catch (IgnoreTaskException e) { + return e.handle(); + } finally { + semaphore.release(); + } + } else { + throw new LimitException("No more permits available for the semaphore"); + } + } + + @Override + public void invoke(Runnable runnable) throws Exception { + if (semaphore.tryAcquire()) { + try { + runnable.run(); + } catch (IgnoreTaskException e) { + e.handle(); + } finally { + semaphore.release(); + } + } else { + throw new LimitException("No more permits available for the semaphore"); + } + } + + @Override + public Optional tryAcquire(boolean wait) { + if (!semaphore.tryAcquire()) { + return Optional.empty(); + } + return Optional.of(new SemaphoreToken(semaphore)); + } + + @Override + public Semaphore semaphore() { + return semaphore; + } + } + + private static class QueuedSemaphoreHandler implements LimiterHandler { + private final Semaphore semaphore; + private final int queueLength; + private final long timeoutMillis; + + private QueuedSemaphoreHandler(Semaphore semaphore, int queueLength, Duration queueTimeout) { + this.semaphore = semaphore; + this.queueLength = queueLength; + this.timeoutMillis = queueTimeout.toMillis(); + } + + @Override + public Optional tryAcquire(boolean wait) { + if (semaphore.getQueueLength() >= this.queueLength) { + // this is an estimate - we do not promise to be precise here + return Optional.empty(); + } + + try { + if (wait) { + if (!semaphore.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)) { + return Optional.empty(); + } + } else { + if (!semaphore.tryAcquire()) { + return Optional.empty(); + } + } + + } catch (InterruptedException e) { + return Optional.empty(); + } + return Optional.of(new SemaphoreToken(semaphore)); + } + + @Override + public Semaphore semaphore() { + return semaphore; + } + } + + private static class SemaphoreToken implements Token { + private final Semaphore semaphore; + + private SemaphoreToken(Semaphore semaphore) { + this.semaphore = semaphore; + } + + @Override + public void dropped() { + semaphore.release(); + } + + @Override + public void ignore() { + semaphore.release(); + } + + @Override + public void success() { + semaphore.release(); + } + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitConfigBlueprint.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitConfigBlueprint.java new file mode 100644 index 00000000000..c5e672c70cd --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitConfigBlueprint.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.Semaphore; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.concurrency.limits.spi.LimitProvider; + +/** + * Configuration of {@link FixedLimit}. + * + * @see #permits() + * @see #queueLength() + * @see #queueTimeout() + */ +@Prototype.Blueprint +@Prototype.Configured(value = FixedLimit.TYPE, root = false) +@Prototype.Provides(LimitProvider.class) +interface FixedLimitConfigBlueprint extends Prototype.Factory { + /** + * Number of permit to allow. + * Defaults to {@value FixedLimit#DEFAULT_LIMIT}. + * When set to {@code 0}, we switch to unlimited. + * + * @return number of permits + */ + @Option.Configured + @Option.DefaultInt(FixedLimit.DEFAULT_LIMIT) + int permits(); + + /** + * Whether the {@link java.util.concurrent.Semaphore} should be {@link java.util.concurrent.Semaphore#isFair()}. + * Defaults to {@code false}. + * + * @return whether this should be a fair semaphore + */ + @Option.Configured + @Option.DefaultBoolean(false) + boolean fair(); + + /** + * How many requests can be enqueued waiting for a permit. + * Note that this may not be an exact behavior due to concurrent invocations. + * We use {@link java.util.concurrent.Semaphore#getQueueLength()} in the + * {@link io.helidon.common.concurrency.limits.FixedLimit} implementation. + * Default value is {@value FixedLimit#DEFAULT_QUEUE_LENGTH}. + * If set to {code 0}, there is no queueing. + * + * @return number of requests to enqueue + */ + @Option.Configured + @Option.DefaultInt(FixedLimit.DEFAULT_QUEUE_LENGTH) + int queueLength(); + + /** + * How long to wait for a permit when enqueued. + * Defaults to {@value FixedLimit#DEFAULT_QUEUE_TIMEOUT_DURATION} + * + * @return duration of the timeout + */ + @Option.Configured + @Option.Default(FixedLimit.DEFAULT_QUEUE_TIMEOUT_DURATION) + Duration queueTimeout(); + + /** + * Name of this instance. + * + * @return name of the instance + */ + @Option.Default(FixedLimit.TYPE) + String name(); + + /** + * Explicitly configured semaphore. + * Note that if this is set, all other configuration is ignored. + * + * @return semaphore instance + */ + Optional semaphore(); + +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitProvider.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitProvider.java new file mode 100644 index 00000000000..2d221579bf1 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/FixedLimitProvider.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import io.helidon.common.Weight; +import io.helidon.common.concurrency.limits.spi.LimitProvider; +import io.helidon.common.config.Config; + +/** + * {@link java.util.ServiceLoader} service provider for {@link FixedLimit} + * limit implementation. + */ +@Weight(90) +public class FixedLimitProvider implements LimitProvider { + /** + * Constructor required by the service loader. + */ + public FixedLimitProvider() { + } + + @Override + public String configKey() { + return FixedLimit.TYPE; + } + + @Override + public Limit create(Config config, String name) { + return FixedLimit.builder() + .config(config) + .name(name) + .build(); + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/IgnoreTaskException.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/IgnoreTaskException.java new file mode 100644 index 00000000000..e6e7458fb89 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/IgnoreTaskException.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.Objects; + +/** + * If this exception is thrown from a limited task within + * {@link Limit#invoke(java.util.concurrent.Callable)}, the + * invocation will be ignored by possible algorithms (for example when considering round-trip timing). + *

+ * This should be used for cases where we never got to execute the intended task. + * This exception should never be thrown by {@link Limit}, it should always + * be translated to a proper return type, or actual exception. + */ +public class IgnoreTaskException extends RuntimeException { + /** + * Desired return value, if we want to ignore the result, yet we still provide valid response. + */ + private final Object returnValue; + /** + * Exception to throw to the user. This is to allow throwing an exception while ignoring it for limits algorithm. + */ + private final Exception exception; + + /** + * Create a new instance with a cause. + * + * @param cause the cause of this exception + */ + public IgnoreTaskException(Exception cause) { + super(Objects.requireNonNull(cause)); + + this.exception = cause; + this.returnValue = null; + } + + /** + * Create a new instance with a return value. + * + * @param returnValue value to return, even though this invocation should be ignored + * return value may be {@code null}. + */ + public IgnoreTaskException(Object returnValue) { + this.exception = null; + this.returnValue = returnValue; + } + + /** + * This is used by limit implementations to either return the value, or throw an exception. + * + * @return the value provided to be the return value + * @param type of the return value + * @throws Exception exception provided by the task + */ + @SuppressWarnings("unchecked") + public T handle() throws Exception { + if (returnValue == null && exception != null) { + throw exception; + } + return (T) returnValue; + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/Limit.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/Limit.java new file mode 100644 index 00000000000..fc375762006 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/Limit.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import io.helidon.common.config.NamedService; +import io.helidon.service.registry.Service; + +/** + * Contract for a concurrency limiter. + */ +@Service.Contract +public interface Limit extends LimitAlgorithm, NamedService { + /** + * Create a copy of this limit with the same configuration. + * + * @return a copy of this limit + */ + Limit copy(); +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitAlgorithm.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitAlgorithm.java new file mode 100644 index 00000000000..c81761abb65 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitAlgorithm.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.Optional; +import java.util.concurrent.Callable; + +/** + * Concurrency limit algorithm. + *

+ * There are two options how to use a limit - by handling a token provided by {@link #tryAcquire()}, + * or by invoking a callable or runnable through one of the invoke methods (such as {@link #invoke(Runnable)}. + *

+ * The invoke methods are backed by the same {@link #tryAcquire()} methods, so behavior is consistent. + */ +public interface LimitAlgorithm { + /** + * Invoke a callable within the limits of this limiter. + *

+ * {@link io.helidon.common.concurrency.limits.Limit} implementors note: + * Make sure to catch {@link io.helidon.common.concurrency.limits.IgnoreTaskException} from the + * callable, and call its {@link IgnoreTaskException#handle()} to either return the provided result, + * or throw the exception after ignoring the timing for future decisions. + * + * @param callable callable to execute within the limit + * @param the callable return type + * @return result of the callable + * @throws LimitException in case the limiter did not have an available permit + * @throws java.lang.Exception in case the task failed with an exception + */ + default T invoke(Callable callable) throws LimitException, Exception { + Optional token = tryAcquire(); + if (token.isEmpty()) { + throw new LimitException("No token available."); + } + Token permit = token.get(); + try { + T response = callable.call(); + permit.success(); + return response; + } catch (IgnoreTaskException e) { + permit.ignore(); + return e.handle(); + } catch (Exception e) { + permit.dropped(); + throw e; + } + } + + /** + * Invoke a runnable within the limits of this limiter. + *

+ * {@link io.helidon.common.concurrency.limits.Limit} implementors note: + * Make sure to catch {@link io.helidon.common.concurrency.limits.IgnoreTaskException} from the + * runnable, and call its {@link IgnoreTaskException#handle()} to either return the provided result, + * or throw the exception after ignoring the timing for future decisions. + * + * @param runnable runnable to execute within the limit + * @throws LimitException in case the limiter did not have an available permit + * @throws java.lang.Exception in case the task failed with an exception + */ + default void invoke(Runnable runnable) throws LimitException, Exception { + Optional token = tryAcquire(); + if (token.isEmpty()) { + throw new LimitException("No token available."); + } + Token permit = token.get(); + try { + runnable.run(); + permit.success(); + } catch (IgnoreTaskException e) { + permit.ignore(); + e.handle(); + } catch (Exception e) { + permit.dropped(); + throw e; + } + } + + /** + * Try to acquire a token, waiting for available permits for the configured amount of time, if queuing is enabled. + *

+ * If acquired, the caller must call one of the {@link io.helidon.common.concurrency.limits.Limit.Token} + * operations to release the token. + * If the response is empty, the limit does not have an available token. + * + * @return acquired token, or empty if there is no available token + */ + default Optional tryAcquire() { + return tryAcquire(true); + } + + /** + * Try to acquire a token, waiting for available permits for the configured amount of time, if + * {@code wait} is enabled, returning immediately otherwise. + *

+ * If acquired, the caller must call one of the {@link io.helidon.common.concurrency.limits.Limit.Token} + * operations to release the token. + * If the response is empty, the limit does not have an available token. + * + * @param wait whether to wait in the queue (if one is configured/available in the limit), or to return immediately + * @return acquired token, or empty if there is no available token + */ + Optional tryAcquire(boolean wait); + + /** + * When a token is retrieved from {@link #tryAcquire()}, one of its methods must be called when the task + * is over, to release the token back to the pool (such as a permit returned to a {@link java.util.concurrent.Semaphore}). + *

+ * Choice of method to invoke may influence the algorithm used for determining number of available permits. + */ + interface Token { + /** + * Operation was dropped, for example because it hit a timeout, or was rejected by other limits. + * Loss based {@link io.helidon.common.concurrency.limits.Limit} implementations will likely do an aggressive + * reducing in limit when this happens. + */ + void dropped(); + + /** + * The operation failed before any meaningful RTT measurement could be made and should be ignored to not + * introduce an artificially low RTT. + */ + void ignore(); + + /** + * Notification that the operation succeeded and internally measured latency should be used as an RTT sample. + */ + void success(); + } +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitException.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitException.java new file mode 100644 index 00000000000..987d9cf99fa --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/LimitException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.Objects; + +/** + * A limit was reached and the submitted task cannot be executed. + * + * @see io.helidon.common.concurrency.limits.Limit#invoke(java.util.concurrent.Callable) + * @see io.helidon.common.concurrency.limits.Limit#invoke(Runnable) + */ +public class LimitException extends RuntimeException { + /** + * A new limit exception with a cause. + * + * @param cause cause of the limit reached + */ + public LimitException(Exception cause) { + super(Objects.requireNonNull(cause)); + } + + /** + * A new limit exception with a message. + * + * @param message description of why the limit was reached + */ + public LimitException(String message) { + super(Objects.requireNonNull(message)); + } +} diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/NoopSemaphore.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/NoopSemaphore.java similarity index 79% rename from webserver/webserver/src/main/java/io/helidon/webserver/NoopSemaphore.java rename to common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/NoopSemaphore.java index a20c8790089..b86210e0f77 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/NoopSemaphore.java +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/NoopSemaphore.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,18 +14,28 @@ * limitations under the License. */ -package io.helidon.webserver; +package io.helidon.common.concurrency.limits; import java.util.Collection; import java.util.Set; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -/* +/** * A semaphore that does nothing. + * Use {@link #INSTANCE} to get an instance of this semaphore. + * + * @deprecated this is only provided for backward compatibility and will be removed, use + * {@link FixedLimit#create()} to get unlimited limit */ -class NoopSemaphore extends Semaphore { - NoopSemaphore() { +@Deprecated(forRemoval = true, since = "4.2.0") +public class NoopSemaphore extends Semaphore { + /** + * Singleton instance to be used whenever needed. + */ + public static final Semaphore INSTANCE = new NoopSemaphore(); + + private NoopSemaphore() { super(0); } diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/SemaphoreLimit.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/SemaphoreLimit.java new file mode 100644 index 00000000000..77332ce397b --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/SemaphoreLimit.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.concurrent.Semaphore; + +/** + * The {@link io.helidon.common.concurrency.limits.Limit} is backed by a semaphore, and this provides + * direct access to the semaphore. + * Note that this usage may bypass calculation of limits if the semaphore is used directly. + * This is for backward compatibility only, and will be removed. + * + * @deprecated DO NOT USE except for backward compatibility with semaphore based handling + */ +@Deprecated(since = "4.2.0", forRemoval = true) +public interface SemaphoreLimit { + /** + * Underlying semaphore of this limit. + * + * @return the semaphore instance + * @deprecated this only exists for backward compatibility of Helidon WebServer and will be removed + */ + @Deprecated(forRemoval = true, since = "4.2.0") + Semaphore semaphore(); +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/package-info.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/package-info.java new file mode 100644 index 00000000000..2804c1ef810 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Concurrency limits API and default implementations. + * + * @see io.helidon.common.concurrency.limits.Limit + * @see io.helidon.common.concurrency.limits.FixedLimit + */ +package io.helidon.common.concurrency.limits; diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/LimitProvider.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/LimitProvider.java new file mode 100644 index 00000000000..c192b734ff9 --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/LimitProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits.spi; + +import io.helidon.common.concurrency.limits.Limit; +import io.helidon.common.config.ConfiguredProvider; +import io.helidon.service.registry.Service; + +/** + * A {@link java.util.ServiceLoader} (and service registry) service provider to discover rate limits. + */ +@Service.Contract +public interface LimitProvider extends ConfiguredProvider { +} diff --git a/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/package-info.java b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/package-info.java new file mode 100644 index 00000000000..e88d31d397d --- /dev/null +++ b/common/concurrency/limits/src/main/java/io/helidon/common/concurrency/limits/spi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Extension points to create custom concurrency rate limits. + */ +package io.helidon.common.concurrency.limits.spi; diff --git a/common/concurrency/limits/src/main/java/module-info.java b/common/concurrency/limits/src/main/java/module-info.java new file mode 100644 index 00000000000..f1b23377399 --- /dev/null +++ b/common/concurrency/limits/src/main/java/module-info.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Concurrency limits. + * + * @see io.helidon.common.concurrency.limits + */ +module io.helidon.common.concurrency.limits { + requires static io.helidon.service.registry; + + requires io.helidon.builder.api; + requires io.helidon.common; + requires io.helidon.common.config; + + exports io.helidon.common.concurrency.limits; + exports io.helidon.common.concurrency.limits.spi; + + provides io.helidon.common.concurrency.limits.spi.LimitProvider + with io.helidon.common.concurrency.limits.FixedLimitProvider, + io.helidon.common.concurrency.limits.AimdLimitProvider; +} \ No newline at end of file diff --git a/common/concurrency/limits/src/main/resources/META-INF/helidon/service.loader b/common/concurrency/limits/src/main/resources/META-INF/helidon/service.loader new file mode 100644 index 00000000000..e2dd011e9fa --- /dev/null +++ b/common/concurrency/limits/src/main/resources/META-INF/helidon/service.loader @@ -0,0 +1,2 @@ +# List of service contracts we want to support either from service registry, or from service loader +io.helidon.common.concurrency.limits.spi.LimitProvider diff --git a/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/AimdLimitTest.java b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/AimdLimitTest.java new file mode 100644 index 00000000000..b2f8615eb1d --- /dev/null +++ b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/AimdLimitTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +public class AimdLimitTest { + @Test + void decreaseOnDrops() { + AimdLimitConfig config = AimdLimitConfig.builder() + .initialLimit(30) + .buildPrototype(); + + AimdLimitImpl limiter = new AimdLimitImpl(config); + + assertThat(limiter.currentLimit(), is(30)); + limiter.updateWithSample(0, 0, 0, false); + assertThat(limiter.currentLimit(), is(27)); + } + + @Test + void decreaseOnTimeoutExceeded() { + Duration timeout = Duration.ofSeconds(1); + AimdLimitConfig config = AimdLimitConfig.builder() + .initialLimit(30) + .timeout(timeout) + .buildPrototype(); + AimdLimitImpl limiter = new AimdLimitImpl(config); + limiter.updateWithSample(0, timeout.toNanos() + 1, 0, true); + assertThat(limiter.currentLimit(), is(27)); + } + + @Test + void increaseOnSuccess() { + AimdLimitConfig config = AimdLimitConfig.builder() + .initialLimit(20) + .buildPrototype(); + AimdLimitImpl limiter = new AimdLimitImpl(config); + limiter.updateWithSample(0, Duration.ofMillis(1).toNanos(), 10, true); + assertThat(limiter.currentLimit(), is(21)); + } + + @Test + void successOverflow() { + AimdLimitConfig config = AimdLimitConfig.builder() + .initialLimit(21) + .maxLimit(21) + .minLimit(0) + .buildPrototype(); + AimdLimitImpl limiter = new AimdLimitImpl(config); + limiter.updateWithSample(0, Duration.ofMillis(1).toNanos(), 10, true); + // after success limit should still be at the max. + assertThat(limiter.currentLimit(), is(21)); + } + + @Test + void testDefault() { + AimdLimitConfig config = AimdLimitConfig.builder() + .minLimit(10) + .initialLimit(10) + .buildPrototype(); + AimdLimitImpl limiter = new AimdLimitImpl(config); + assertThat(limiter.currentLimit(), is(10)); + } + + @Test + void concurrentUpdatesAndReads() throws InterruptedException { + AimdLimitConfig config = AimdLimitConfig.builder() + .initialLimit(1) + .backoffRatio(0.9) + .timeout(Duration.ofMillis(100)) + .minLimit(1) + .maxLimit(200) + .buildPrototype(); + AimdLimitImpl limit = new AimdLimitImpl(config); + + int threadCount = 100; + int operationsPerThread = 1_000; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threadCount); + + AtomicInteger successCount = new AtomicInteger(0); + AtomicInteger timeoutCount = new AtomicInteger(0); + AtomicInteger dropCount = new AtomicInteger(0); + + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + try { + startLatch.await(); // Wait for all threads to be ready + for (int j = 0; j < operationsPerThread; j++) { + long startTime = System.nanoTime(); + long rtt = (long) (Math.random() * 200_000_000); // 0-200ms + int concurrentRequests = (int) (Math.random() * limit.currentLimit() * 2); + boolean didDrop = Math.random() < 0.01; // 1% chance of drop + + limit.updateWithSample(startTime, rtt, concurrentRequests, !didDrop); + + if (didDrop) { + dropCount.incrementAndGet(); + } else if (rtt > config.timeout().toNanos()) { + timeoutCount.incrementAndGet(); + } else { + successCount.incrementAndGet(); + } + + // Read the current limit + int currentLimit = limit.currentLimit(); + assertThat(currentLimit, is(greaterThanOrEqualTo(config.minLimit()))); + assertThat(currentLimit, is(lessThanOrEqualTo(config.maxLimit()))); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + endLatch.countDown(); + } + }); + } + + startLatch.countDown(); // Start all threads + boolean finished = endLatch.await(10, TimeUnit.SECONDS); + executor.shutdown(); + + assertThat("Test did not complete in time", finished, is(true)); + + assertThat("Total operations mismatch", + threadCount * operationsPerThread, + is(successCount.get() + timeoutCount.get() + dropCount.get())); + } + + @Test + public void testSemaphoreReleased() throws Exception { + Limit limit = AimdLimit.builder() + .minLimit(5) + .initialLimit(5) + .build(); + + for (int i = 0; i < 5000; i++) { + limit.invoke(() -> {}); + } + } +} diff --git a/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/ConfiguredLimitTest.java b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/ConfiguredLimitTest.java new file mode 100644 index 00000000000..9c9a79e96bb --- /dev/null +++ b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/ConfiguredLimitTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.Optional; + +import io.helidon.config.Config; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +public class ConfiguredLimitTest { + private static Config config; + + @BeforeAll + public static void init() { + config = Config.create(); + } + + @Test + public void testFixed() { + LimitUsingConfig limitConfig = LimitUsingConfig.create(config.get("first")); + Optional configuredLimit = limitConfig.concurrencyLimit(); + assertThat(configuredLimit, not(Optional.empty())); + Limit limit = configuredLimit.get(); + + assertThat(limit.name(), is("server-listener")); + assertThat(limit.type(), is("fixed")); + + FixedLimitConfig prototype = ((FixedLimit) limit).prototype(); + assertThat("Permits", prototype.permits(), is(1)); + assertThat("Queue length", prototype.queueLength(), is(20)); + assertThat("Should be fair", prototype.fair(), is(true)); + assertThat("Queue timeout", prototype.queueTimeout(), is(Duration.ofSeconds(42))); + } + + @Test + public void testAimd() { + LimitUsingConfig limitConfig = LimitUsingConfig.create(config.get("second")); + Optional configuredLimit = limitConfig.concurrencyLimit(); + assertThat(configuredLimit, not(Optional.empty())); + Limit limit = configuredLimit.get(); + + assertThat(limit.name(), is("aimd")); + assertThat(limit.type(), is("aimd")); + + AimdLimitConfig prototype = ((AimdLimit) limit).prototype(); + assertThat("Timeout", prototype.timeout(), is(Duration.ofSeconds(42))); + assertThat("Min limit", prototype.minLimit(), is(11)); + assertThat("Max limit", prototype.maxLimit(), is(22)); + assertThat("Initial limit", prototype.initialLimit(), is(14)); + assertThat("Backoff ratio", prototype.backoffRatio(), is(0.74)); + } +} diff --git a/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/FixedLimitTest.java b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/FixedLimitTest.java new file mode 100644 index 00000000000..d9f8fca15e1 --- /dev/null +++ b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/FixedLimitTest.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +public class FixedLimitTest { + @Test + public void testUnlimited() throws InterruptedException { + FixedLimit limiter = FixedLimit.create(); + int concurrency = 5; + CountDownLatch cdl = new CountDownLatch(1); + CountDownLatch threadsCdl = new CountDownLatch(concurrency); + + Lock lock = new ReentrantLock(); + List result = new ArrayList<>(concurrency); + + Thread[] threads = new Thread[concurrency]; + for (int i = 0; i < concurrency; i++) { + int index = i; + threads[i] = new Thread(() -> { + try { + limiter.invoke(() -> { + threadsCdl.countDown(); + cdl.await(10, TimeUnit.SECONDS); + lock.lock(); + try { + result.add("result_" + index); + } finally { + lock.unlock(); + } + return null; + }); + } catch (Exception e) { + threadsCdl.countDown(); + throw new RuntimeException(e); + } + }); + } + for (Thread thread : threads) { + thread.start(); + } + threadsCdl.await(); + cdl.countDown(); + for (Thread thread : threads) { + thread.join(Duration.ofSeconds(5)); + } + assertThat(result, hasSize(concurrency)); + } + + @Test + public void testLimit() throws Exception { + FixedLimit limiter = FixedLimit.builder() + .permits(1) + .build(); + + int concurrency = 5; + CountDownLatch cdl = new CountDownLatch(1); + CountDownLatch threadsCdl = new CountDownLatch(concurrency); + + Lock lock = new ReentrantLock(); + List result = new ArrayList<>(concurrency); + AtomicInteger failures = new AtomicInteger(); + + Thread[] threads = new Thread[concurrency]; + for (int i = 0; i < concurrency; i++) { + int index = i; + threads[i] = new Thread(() -> { + try { + limiter.invoke(() -> { + threadsCdl.countDown(); + cdl.await(10, TimeUnit.SECONDS); + lock.lock(); + try { + result.add("result_" + index); + } finally { + lock.unlock(); + } + return null; + }); + } catch (LimitException e) { + threadsCdl.countDown(); + failures.incrementAndGet(); + } catch (Exception e) { + threadsCdl.countDown(); + throw new RuntimeException(e); + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + // wait for all threads to reach appropriate destination + threadsCdl.await(); + cdl.countDown(); + for (Thread thread : threads) { + thread.join(Duration.ofSeconds(5)); + } + assertThat(failures.get(), is(concurrency - 1)); + assertThat(result.size(), is(1)); + } + + @Test + public void testLimitWithQueue() throws Exception { + FixedLimit limiter = FixedLimit.builder() + .permits(1) + .queueLength(1) + .queueTimeout(Duration.ofSeconds(5)) + .build(); + + int concurrency = 5; + CountDownLatch cdl = new CountDownLatch(1); + + Lock lock = new ReentrantLock(); + List result = new ArrayList<>(concurrency); + AtomicInteger failures = new AtomicInteger(); + + Thread[] threads = new Thread[concurrency]; + for (int i = 0; i < concurrency; i++) { + int index = i; + threads[i] = new Thread(() -> { + try { + limiter.invoke(() -> { + cdl.await(10, TimeUnit.SECONDS); + lock.lock(); + try { + result.add("result_" + index); + } finally { + lock.unlock(); + } + return null; + }); + } catch (LimitException e) { + failures.incrementAndGet(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + // wait for the threads to reach their destination (either failed, or on cdl, or in queue) + TimeUnit.MILLISECONDS.sleep(100); + cdl.countDown(); + for (Thread thread : threads) { + thread.join(Duration.ofSeconds(5)); + } + // 1 submitted, 1 in queue (may be less failures, as the queue length is not guaranteed to be atomic + assertThat(failures.get(), lessThanOrEqualTo(concurrency - 2)); + // may be 2 or more (1 submitted, 1 or more queued) + assertThat(result.size(), greaterThanOrEqualTo(2)); + } + + @Test + public void testSemaphoreReleased() throws Exception { + Limit limit = FixedLimit.builder() + .permits(5) + .build(); + + for (int i = 0; i < 5000; i++) { + limit.invoke(() -> { + }); + } + } + + @Test + public void testSemaphoreReleasedWithQueue() throws Exception { + Limit limit = FixedLimit.builder() + .permits(5) + .queueLength(10) + .queueTimeout(Duration.ofMillis(100)) + .build(); + + for (int i = 0; i < 5000; i++) { + limit.invoke(() -> { + }); + } + } +} diff --git a/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/LimitUsingConfigBlueprint.java b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/LimitUsingConfigBlueprint.java new file mode 100644 index 00000000000..35d99bb95c2 --- /dev/null +++ b/common/concurrency/limits/src/test/java/io/helidon/common/concurrency/limits/LimitUsingConfigBlueprint.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.concurrency.limits; + +import java.util.Optional; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.concurrency.limits.spi.LimitProvider; + +@Prototype.Blueprint +@Prototype.Configured +interface LimitUsingConfigBlueprint { + @Option.Provider(value = LimitProvider.class, discoverServices = false) + @Option.Configured + Optional concurrencyLimit(); +} diff --git a/common/concurrency/limits/src/test/resources/application.yaml b/common/concurrency/limits/src/test/resources/application.yaml new file mode 100644 index 00000000000..69540cde1d1 --- /dev/null +++ b/common/concurrency/limits/src/test/resources/application.yaml @@ -0,0 +1,32 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +first: + concurrency-limit: + - type: "fixed" + name: "server-listener" + fair: true + permits: 1 + queue-length: 20 + queue-timeout: "PT42S" +second: + concurrency-limit: + aimd: + timeout: "PT42S" + min-limit: 11 + max-limit: 22 + initial-limit: 14 + backoff-ratio: 0.74 diff --git a/common/concurrency/pom.xml b/common/concurrency/pom.xml new file mode 100644 index 00000000000..1346a2b660a --- /dev/null +++ b/common/concurrency/pom.xml @@ -0,0 +1,39 @@ + + + + + 4.0.0 + + io.helidon.common + helidon-common-project + 4.2.0-SNAPSHOT + ../pom.xml + + + io.helidon.common.concurrency + helidon-common-concurrency-project + Helidon Common Concurrency Project + + pom + + + limits + + diff --git a/common/config/pom.xml b/common/config/pom.xml index 2347a0bdb91..b9238f1f0e9 100644 --- a/common/config/pom.xml +++ b/common/config/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-config Helidon Common Config diff --git a/common/configurable/pom.xml b/common/configurable/pom.xml index e98daecbe52..ff539382455 100644 --- a/common/configurable/pom.xml +++ b/common/configurable/pom.xml @@ -24,7 +24,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT Helidon Common Configurable helidon-common-configurable diff --git a/common/context/pom.xml b/common/context/pom.xml index fdff8e77215..d8ebc45383c 100644 --- a/common/context/pom.xml +++ b/common/context/pom.xml @@ -22,7 +22,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-context Helidon Common Context diff --git a/common/crypto/pom.xml b/common/crypto/pom.xml index 5828acfdb81..9befb1e3286 100644 --- a/common/crypto/pom.xml +++ b/common/crypto/pom.xml @@ -21,7 +21,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/common/features/api/pom.xml b/common/features/api/pom.xml index 0c4b3871ecc..c1afd0727ed 100644 --- a/common/features/api/pom.xml +++ b/common/features/api/pom.xml @@ -23,7 +23,7 @@ io.helidon.common.features helidon-common-features-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-features-api diff --git a/common/features/features/pom.xml b/common/features/features/pom.xml index 17f482113c7..914423afd21 100644 --- a/common/features/features/pom.xml +++ b/common/features/features/pom.xml @@ -23,7 +23,7 @@ io.helidon.common.features helidon-common-features-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-features diff --git a/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java b/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java index 6fd7f937c01..35db736c3a4 100644 --- a/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java +++ b/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,8 @@ static List features(ClassLoader classLoader) { } String module = props.getProperty("m"); if (module == null) { - LOGGER.log(Level.WARNING, "Got module descriptor with no module name. Available properties: " + props); + LOGGER.log(Level.WARNING, "Got module descriptor with no module name. Available properties: " + props + + " at " + url); continue; } FeatureDescriptor.Builder builder = FeatureDescriptor.builder(); diff --git a/common/features/pom.xml b/common/features/pom.xml index 3772c5b59fb..51f6fcf32c6 100644 --- a/common/features/pom.xml +++ b/common/features/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.common.features diff --git a/common/features/processor/pom.xml b/common/features/processor/pom.xml index 4bc2fffecf3..dedd590ccec 100644 --- a/common/features/processor/pom.xml +++ b/common/features/processor/pom.xml @@ -23,7 +23,7 @@ io.helidon.common.features helidon-common-features-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-features-processor Helidon Common Features Annotation Processor diff --git a/common/key-util/pom.xml b/common/key-util/pom.xml index 473ac884652..9f7c9ebd634 100644 --- a/common/key-util/pom.xml +++ b/common/key-util/pom.xml @@ -24,7 +24,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-key-util Helidon Common Key Util diff --git a/common/mapper/pom.xml b/common/mapper/pom.xml index ea9309098cd..567fb46285c 100644 --- a/common/mapper/pom.xml +++ b/common/mapper/pom.xml @@ -21,7 +21,7 @@ helidon-common-project io.helidon.common - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/common/media-type/pom.xml b/common/media-type/pom.xml index da1b6ccfc4c..66af20b4b36 100644 --- a/common/media-type/pom.xml +++ b/common/media-type/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-media-type diff --git a/common/parameters/pom.xml b/common/parameters/pom.xml index 18f9f5fc0d0..a40287075e7 100644 --- a/common/parameters/pom.xml +++ b/common/parameters/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-parameters Helidon Common Parameters diff --git a/common/pom.xml b/common/pom.xml index 898d6543c98..b62636185ab 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.common helidon-common-project @@ -55,6 +55,7 @@ tls types uri + concurrency diff --git a/common/processor/class-model/pom.xml b/common/processor/class-model/pom.xml index 283594c5b4e..d8b8a605312 100644 --- a/common/processor/class-model/pom.xml +++ b/common/processor/class-model/pom.xml @@ -22,7 +22,7 @@ io.helidon.common.processor helidon-common-processor-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-processor-class-model diff --git a/common/processor/helidon-copyright/pom.xml b/common/processor/helidon-copyright/pom.xml index 893bae1907a..c8295af88f1 100644 --- a/common/processor/helidon-copyright/pom.xml +++ b/common/processor/helidon-copyright/pom.xml @@ -23,7 +23,7 @@ io.helidon.common.processor helidon-common-processor-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-processor-helidon-copyright diff --git a/common/processor/pom.xml b/common/processor/pom.xml index e042fbe1d66..481b7feb27f 100644 --- a/common/processor/pom.xml +++ b/common/processor/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/common/processor/processor/pom.xml b/common/processor/processor/pom.xml index 7749de2526b..85b9a054265 100644 --- a/common/processor/processor/pom.xml +++ b/common/processor/processor/pom.xml @@ -23,7 +23,7 @@ io.helidon.common.processor helidon-common-processor-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-processor diff --git a/common/reactive/pom.xml b/common/reactive/pom.xml index e97add8a485..1ee0d9e3ea1 100644 --- a/common/reactive/pom.xml +++ b/common/reactive/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-reactive Helidon Common Reactive diff --git a/common/security/pom.xml b/common/security/pom.xml index fe5853f43db..5884db66599 100644 --- a/common/security/pom.xml +++ b/common/security/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-security Helidon Common Security diff --git a/common/socket/pom.xml b/common/socket/pom.xml index af0096ac6ad..71c9d06ea27 100644 --- a/common/socket/pom.xml +++ b/common/socket/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-socket Helidon Common Socket diff --git a/common/socket/src/main/java/io/helidon/common/socket/SmartSocketWriter.java b/common/socket/src/main/java/io/helidon/common/socket/SmartSocketWriter.java new file mode 100644 index 00000000000..1d310535b4b --- /dev/null +++ b/common/socket/src/main/java/io/helidon/common/socket/SmartSocketWriter.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.socket; + +import java.util.concurrent.ExecutorService; + +import io.helidon.common.buffers.BufferData; + +/** + * A special socket write that starts async but may switch to sync mode if it + * detects that the async queue size is below {@link #QUEUE_SIZE_THRESHOLD}. + * If it switches to sync mode, it shall never return back to async mode. + */ +public class SmartSocketWriter extends SocketWriter { + private static final long WINDOW_SIZE = 1000; + private static final double QUEUE_SIZE_THRESHOLD = 2.0; + + private final SocketWriterAsync asyncWriter; + private volatile long windowIndex; + private volatile boolean asyncMode; + + SmartSocketWriter(ExecutorService executor, HelidonSocket socket, int writeQueueLength) { + super(socket); + this.asyncWriter = new SocketWriterAsync(executor, socket, writeQueueLength); + this.asyncMode = true; + this.windowIndex = 0L; + } + + @Override + public void write(BufferData... buffers) { + for (BufferData buffer : buffers) { + write(buffer); + } + } + + @Override + public void write(BufferData buffer) { + if (asyncMode) { + asyncWriter.write(buffer); + if (++windowIndex % WINDOW_SIZE == 0 && asyncWriter.avgQueueSize() < QUEUE_SIZE_THRESHOLD) { + asyncMode = false; + } + } else { + asyncWriter.drainQueue(); + writeNow(buffer); // blocking write + } + } +} diff --git a/common/socket/src/main/java/io/helidon/common/socket/SocketWriter.java b/common/socket/src/main/java/io/helidon/common/socket/SocketWriter.java index 7d2480c02e3..f5323ab6360 100644 --- a/common/socket/src/main/java/io/helidon/common/socket/SocketWriter.java +++ b/common/socket/src/main/java/io/helidon/common/socket/SocketWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,15 +44,19 @@ protected SocketWriter(HelidonSocket socket) { * @param socket socket to write to * @param writeQueueLength maximal number of queued writes, write operation will block if the queue is full; if set to * {code 1} or lower, write queue is disabled and writes are direct to socket (blocking) + * @param smartAsyncWrites flag to enable smart async writes, see {@link io.helidon.common.socket.SmartSocketWriter} * @return a new socket writer */ public static SocketWriter create(ExecutorService executor, HelidonSocket socket, - int writeQueueLength) { + int writeQueueLength, + boolean smartAsyncWrites) { if (writeQueueLength <= 1) { return new SocketWriterDirect(socket); } else { - return new SocketWriterAsync(executor, socket, writeQueueLength); + return smartAsyncWrites + ? new SmartSocketWriter(executor, socket, writeQueueLength) + : new SocketWriterAsync(executor, socket, writeQueueLength); } } diff --git a/common/socket/src/main/java/io/helidon/common/socket/SocketWriterAsync.java b/common/socket/src/main/java/io/helidon/common/socket/SocketWriterAsync.java index 764e273170a..a663191f414 100644 --- a/common/socket/src/main/java/io/helidon/common/socket/SocketWriterAsync.java +++ b/common/socket/src/main/java/io/helidon/common/socket/SocketWriterAsync.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ class SocketWriterAsync extends SocketWriter implements DataWriter { private static final System.Logger LOGGER = System.getLogger(SocketWriterAsync.class.getName()); private static final BufferData CLOSING_TOKEN = BufferData.empty(); + private final ExecutorService executor; private final ArrayBlockingQueue writeQueue; private final CountDownLatch cdl = new CountDownLatch(1); @@ -40,6 +41,7 @@ class SocketWriterAsync extends SocketWriter implements DataWriter { private volatile Throwable caught; private volatile boolean run = true; private Thread thread; + private double avgQueueSize; /** * A new socket writer. @@ -116,7 +118,8 @@ private void run() { CompositeBufferData toWrite = BufferData.createComposite(writeQueue.take()); // wait if the queue is empty // we only want to read a certain amount of data, if somebody writes huge amounts // we could spin here forever and run out of memory - for (int i = 0; i < 1000; i++) { + int queueSize = 1; + for (; queueSize <= 1000; queueSize++) { BufferData newBuf = writeQueue.poll(); // drain ~all elements from the queue, don't wait. if (newBuf == null) { break; @@ -124,6 +127,7 @@ private void run() { toWrite.add(newBuf); } writeNow(toWrite); + avgQueueSize = (avgQueueSize + queueSize) / 2.0; } cdl.countDown(); } catch (Throwable e) { @@ -141,4 +145,15 @@ private void checkRunning() { throw new SocketWriterException(caught); } } + + void drainQueue() { + BufferData buffer; + while ((buffer = writeQueue.poll()) != null) { + writeNow(buffer); + } + } + + double avgQueueSize() { + return avgQueueSize; + } } diff --git a/common/task/pom.xml b/common/task/pom.xml index d6d7d195029..0a7e3989b1c 100644 --- a/common/task/pom.xml +++ b/common/task/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-task Helidon Common Task diff --git a/common/testing/http-junit5/pom.xml b/common/testing/http-junit5/pom.xml index 0eab7a8baee..4289b78ebe9 100644 --- a/common/testing/http-junit5/pom.xml +++ b/common/testing/http-junit5/pom.xml @@ -23,7 +23,7 @@ io.helidon.common.testing helidon-common-testing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-testing-http-junit5 diff --git a/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java b/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java index 92ec6aed91a..75838cd1710 100644 --- a/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java +++ b/common/testing/http-junit5/src/main/java/io/helidon/common/testing/http/junit5/SocketHttpClient.java @@ -605,6 +605,15 @@ public SocketHttpClient sendChunk(String payload) throws IOException { return this; } + /** + * Provides access to underlying socket reader. + * + * @return the reader + */ + public BufferedReader socketReader() { + return socketReader; + } + /** * Override this to send a specific payload. * diff --git a/common/testing/junit5/pom.xml b/common/testing/junit5/pom.xml index eb75fad7f76..7357ab60f38 100644 --- a/common/testing/junit5/pom.xml +++ b/common/testing/junit5/pom.xml @@ -23,7 +23,7 @@ io.helidon.common.testing helidon-common-testing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-testing-junit5 diff --git a/common/testing/pom.xml b/common/testing/pom.xml index eba5d08485d..2341db93845 100644 --- a/common/testing/pom.xml +++ b/common/testing/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.common.testing diff --git a/common/tls/pom.xml b/common/tls/pom.xml index c00431e8440..6132cc27901 100644 --- a/common/tls/pom.xml +++ b/common/tls/pom.xml @@ -24,7 +24,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-tls diff --git a/common/types/pom.xml b/common/types/pom.xml index 25b8cd1ddbb..4a1adc9a36c 100644 --- a/common/types/pom.xml +++ b/common/types/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-types diff --git a/common/types/src/main/java/io/helidon/common/types/Annotated.java b/common/types/src/main/java/io/helidon/common/types/Annotated.java index 292368d72de..3aab5620bdf 100644 --- a/common/types/src/main/java/io/helidon/common/types/Annotated.java +++ b/common/types/src/main/java/io/helidon/common/types/Annotated.java @@ -43,6 +43,8 @@ public interface Annotated { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @return list of all meta annotations of this element */ diff --git a/common/types/src/main/java/io/helidon/common/types/Annotation.java b/common/types/src/main/java/io/helidon/common/types/Annotation.java index f08439985d6..bd27d407599 100644 --- a/common/types/src/main/java/io/helidon/common/types/Annotation.java +++ b/common/types/src/main/java/io/helidon/common/types/Annotation.java @@ -17,8 +17,10 @@ package io.helidon.common.types; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -153,7 +155,9 @@ static Annotation create(TypeName annoTypeName, Map values) { */ abstract class BuilderBase, PROTOTYPE extends Annotation> implements Prototype.Builder { + private final List metaAnnotations = new ArrayList<>(); private final Map values = new LinkedHashMap<>(); + private boolean isMetaAnnotationsMutated; private TypeName typeName; /** @@ -171,6 +175,10 @@ protected BuilderBase() { public BUILDER from(Annotation prototype) { typeName(prototype.typeName()); addValues(prototype.values()); + if (!isMetaAnnotationsMutated) { + metaAnnotations.clear(); + } + addMetaAnnotations(prototype.metaAnnotations()); return self(); } @@ -182,7 +190,15 @@ public BUILDER from(Annotation prototype) { */ public BUILDER from(Annotation.BuilderBase builder) { builder.typeName().ifPresent(this::typeName); - addValues(builder.values()); + addValues(builder.values); + if (isMetaAnnotationsMutated) { + if (builder.isMetaAnnotationsMutated) { + addMetaAnnotations(builder.metaAnnotations); + } + } else { + metaAnnotations.clear(); + addMetaAnnotations(builder.metaAnnotations); + } return self(); } @@ -293,6 +309,64 @@ public BUILDER putValue(String key, Object value) { return self(); } + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param metaAnnotations list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER metaAnnotations(List metaAnnotations) { + Objects.requireNonNull(metaAnnotations); + isMetaAnnotationsMutated = true; + this.metaAnnotations.clear(); + this.metaAnnotations.addAll(metaAnnotations); + return self(); + } + + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param metaAnnotations list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER addMetaAnnotations(List metaAnnotations) { + Objects.requireNonNull(metaAnnotations); + isMetaAnnotationsMutated = true; + this.metaAnnotations.addAll(metaAnnotations); + return self(); + } + + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param metaAnnotation list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER addMetaAnnotation(Annotation metaAnnotation) { + Objects.requireNonNull(metaAnnotation); + this.metaAnnotations.add(metaAnnotation); + isMetaAnnotationsMutated = true; + return self(); + } + + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param consumer list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER addMetaAnnotation(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = Annotation.builder(); + consumer.accept(builder); + this.metaAnnotations.add(builder.build()); + return self(); + } + /** * The type name, e.g., {@link java.util.Objects} -> "java.util.Objects". * @@ -311,6 +385,15 @@ public Map values() { return values; } + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @return the meta annotations + */ + public List metaAnnotations() { + return metaAnnotations; + } + @Override public String toString() { return "AnnotationBuilder{" @@ -341,6 +424,7 @@ protected void validatePrototype() { */ protected static class AnnotationImpl implements Annotation { + private final List metaAnnotations; private final Map values; private final TypeName typeName; @@ -352,6 +436,7 @@ protected static class AnnotationImpl implements Annotation { protected AnnotationImpl(Annotation.BuilderBase builder) { this.typeName = builder.typeName().get(); this.values = Collections.unmodifiableMap(new LinkedHashMap<>(builder.values())); + this.metaAnnotations = List.copyOf(builder.metaAnnotations()); } @Override @@ -369,6 +454,11 @@ public Map values() { return values; } + @Override + public List metaAnnotations() { + return metaAnnotations; + } + @Override public String toString() { return "Annotation{" @@ -386,7 +476,7 @@ public boolean equals(Object o) { return false; } return Objects.equals(typeName, other.typeName()) - && Objects.equals(values, other.values()); + && Objects.equals(values, other.values()); } @Override diff --git a/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java b/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java index 13e2a99d5f6..b51fb659b6d 100644 --- a/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,6 +79,15 @@ interface AnnotationBlueprint { @Option.Singular Map values(); + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @return list of all annotations declared on the annotation type, or inherited from them + */ + @Option.Redundant + @Option.Singular + List metaAnnotations(); + /** * The value property. * @@ -653,4 +662,19 @@ default > Optional> enumValues(String property, Class< return AnnotationSupport.asEnums(typeName(), values(), property, type); } + /** + * Check if {@link io.helidon.common.types.Annotation#metaAnnotations()} contains an annotation of the provided type. + *

+ * Note: we ignore {@link java.lang.annotation.Target}, {@link java.lang.annotation.Inherited}, + * {@link java.lang.annotation.Documented}, and {@link java.lang.annotation.Retention}. + * + * @param annotationType type of annotation + * @return {@code true} if the annotation is declared on this annotation, or is inherited from a declared annotation + */ + default boolean hasMetaAnnotation(TypeName annotationType) { + return metaAnnotations() + .stream() + .map(Annotation::typeName) + .anyMatch(annotationType::equals); + } } diff --git a/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java b/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java index 242940fb555..b21a8a1a917 100644 --- a/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java +++ b/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java @@ -628,18 +628,15 @@ private static Class asClass(TypeName typeName, String property, Object value return theClass; } - String className; - - if (value instanceof TypeName tn) { - className = tn.fqName(); - } else if (value instanceof String str) { - className = str; - } else { - - throw new IllegalArgumentException(typeName.fqName() + " property " + property - + " of type " + value.getClass().getName() - + " cannot be converted to Class"); - } + String className = switch (value) { + case TypeName tn -> tn.name(); + case String str -> str; + default -> { + throw new IllegalArgumentException(typeName.fqName() + " property " + property + + " of type " + value.getClass().getName() + + " cannot be converted to Class"); + } + }; try { return Class.forName(className); diff --git a/common/types/src/main/java/io/helidon/common/types/ElementSignature.java b/common/types/src/main/java/io/helidon/common/types/ElementSignature.java new file mode 100644 index 00000000000..a167313516d --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/ElementSignature.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.List; + +/** + * Signature of a {@link io.helidon.common.types.TypedElementInfo}. + *

+ * The {@link io.helidon.common.types.TypedElementInfo#signature()} is intended to compare + * fields, methods, and constructors across type hierarchy - for example when looking for a method + * that we override. + *

+ * The following information is used for equals and hash-code: + *

    + *
  • Field: field name
  • + *
  • Constructor: parameter types
  • + *
  • Method: method name, parameter types
  • + *
  • Parameter: this signature is not useful, as we cannot depend on parameter names
  • + *
+ * + * The signature has well-defined {@code hashCode} and {@code equals} methods, + * so it can be safely used as a key in a {@link java.util.Map}. + *

+ * This interface is sealed, an instance can only be obtained + * from {@link io.helidon.common.types.TypedElementInfo#signature()}. + * + * @see #text() + */ +public sealed interface ElementSignature permits ElementSignatures.FieldSignature, + ElementSignatures.MethodSignature, + ElementSignatures.ParameterSignature, + ElementSignatures.NoSignature { + /** + * Type of the element. Resolves as follows: + *

    + *
  • Field: type of the field
  • + *
  • Constructor: void
  • + *
  • Method: method return type
  • + *
  • Parameter: parameter type
  • + *
+ * + * @return type of this element, never used for equals or hashCode + */ + TypeName type(); + + /** + * Name of the element. For constructor, this always returns {@code }, + * for parameters, this method may return the real parameter name or an index + * parameter name depending on the source of the information (during annotation processing, + * this would be the actual parameter name, when classpath scanning, this would be something like + * {@code param0}. + * + * @return name of this element + */ + String name(); + + /** + * Types of parameters if this represents a method or a constructor, + * empty {@link java.util.List} otherwise. + * + * @return parameter types + */ + List parameterTypes(); + + /** + * A text representation of this signature. + * + *
    + *
  • Field: field name (such as {@code myNiceField}
  • + *
  • Constructor: comma separated parameter types (no generics) in parentheses (such as + * {@code (java.lang.String,java.util.List)})
  • + *
  • Method: method name, parameter types (no generics) in parentheses (such as + * {@code methodName(java.lang.String,java.util.List)}
  • + *
  • Parameter: parameter name (such as {@code myParameter} or {@code param0} - not very useful, as parameter names + * are not carried over to compiled code in Java
  • + *
+ * + * @return text representation + */ + String text(); +} diff --git a/common/types/src/main/java/io/helidon/common/types/ElementSignatures.java b/common/types/src/main/java/io/helidon/common/types/ElementSignatures.java new file mode 100644 index 00000000000..4f5be563f29 --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/ElementSignatures.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +final class ElementSignatures { + private ElementSignatures() { + } + + static ElementSignature createNone() { + return new NoSignature(); + } + + static ElementSignature createField(TypeName type, + String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new FieldSignature(type, name); + } + + static ElementSignature createConstructor(List parameters) { + Objects.requireNonNull(parameters); + return new MethodSignature(TypeNames.PRIMITIVE_VOID, + "", + parameters); + } + + static ElementSignature createMethod(TypeName returnType, String name, List parameters) { + Objects.requireNonNull(returnType); + Objects.requireNonNull(name); + Objects.requireNonNull(parameters); + return new MethodSignature(returnType, + name, + parameters); + } + + static ElementSignature createParameter(TypeName type, String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new ParameterSignature(type, name); + } + + static final class FieldSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private FieldSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FieldSignature that)) { + return false; + } + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class MethodSignature implements ElementSignature { + private final TypeName type; + private final String name; + private final List parameters; + private final String text; + private final boolean constructor; + + private MethodSignature(TypeName type, + String name, + List parameters) { + this.type = type; + this.name = name; + this.parameters = parameters; + if (name.equals("")) { + this.constructor = true; + this.text = parameterTypesSection(parameters, ",", TypeName::fqName); + } else { + this.constructor = false; + this.text = name + parameterTypesSection(parameters, ",", TypeName::fqName); + } + + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return parameters; + } + + @Override + public String text() { + return text; + } + + @Override + public String toString() { + if (constructor) { + return text; + } else { + return type.resolvedName() + " " + name + parameterTypesSection(parameters, + ", ", + TypeName::resolvedName); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MethodSignature that)) { + return false; + } + return Objects.equals(name, that.name) && Objects.equals(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(name, parameters); + } + } + + static final class ParameterSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private ParameterSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ParameterSignature that)) { + return false; + } + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class NoSignature implements ElementSignature { + @Override + public TypeName type() { + return TypeNames.PRIMITIVE_VOID; + } + + @Override + public String name() { + return ""; + } + + @Override + public String text() { + return ""; + } + + @Override + public String toString() { + return text(); + } + + @Override + public List parameterTypes() { + return List.of(); + } + } + + private static String parameterTypesSection(List parameters, + String delimiter, + Function typeMapper) { + return parameters.stream() + .map(typeMapper) + .collect(Collectors.joining(delimiter, "(", ")")); + } +} diff --git a/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java b/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java index 76bd62f24dc..f2205b2452e 100644 --- a/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java +++ b/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java @@ -52,4 +52,9 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(type, name); } + + @Override + public String toString() { + return type.fqName() + "." + name; + } } diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfo.java b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java index d47b9f04df1..cc027638df8 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfo.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java @@ -75,6 +75,11 @@ abstract class BuilderBase elementModifiers = new LinkedHashSet<>(); private final Set modifiers = new LinkedHashSet<>(); private AccessModifier accessModifier; + private boolean isAnnotationsMutated; + private boolean isElementInfoMutated; + private boolean isInheritedAnnotationsMutated; + private boolean isInterfaceTypeInfoMutated; + private boolean isOtherElementInfoMutated; private ElementKind kind; private Object originatingElement; private String description; @@ -90,7 +95,7 @@ protected BuilderBase() { } /** - * Update this builder from an existing prototype instance. + * Update this builder from an existing prototype instance. This method disables automatic service discovery. * * @param prototype existing prototype to update this builder from * @return updated builder instance @@ -100,18 +105,33 @@ public BUILDER from(TypeInfo prototype) { description(prototype.description()); typeKind(prototype.typeKind()); kind(prototype.kind()); + if (!isElementInfoMutated) { + elementInfo.clear(); + } addElementInfo(prototype.elementInfo()); + if (!isOtherElementInfoMutated) { + otherElementInfo.clear(); + } addOtherElementInfo(prototype.otherElementInfo()); addReferencedTypeNamesToAnnotations(prototype.referencedTypeNamesToAnnotations()); addReferencedModuleNames(prototype.referencedModuleNames()); superTypeInfo(prototype.superTypeInfo()); + if (!isInterfaceTypeInfoMutated) { + interfaceTypeInfo.clear(); + } addInterfaceTypeInfo(prototype.interfaceTypeInfo()); addModifiers(prototype.modifiers()); addElementModifiers(prototype.elementModifiers()); accessModifier(prototype.accessModifier()); module(prototype.module()); originatingElement(prototype.originatingElement()); + if (!isAnnotationsMutated) { + annotations.clear(); + } addAnnotations(prototype.annotations()); + if (!isInheritedAnnotationsMutated) { + inheritedAnnotations.clear(); + } addInheritedAnnotations(prototype.inheritedAnnotations()); return self(); } @@ -127,19 +147,54 @@ public BUILDER from(TypeInfo.BuilderBase builder) { builder.description().ifPresent(this::description); builder.typeKind().ifPresent(this::typeKind); builder.kind().ifPresent(this::kind); - addElementInfo(builder.elementInfo()); - addOtherElementInfo(builder.otherElementInfo()); - addReferencedTypeNamesToAnnotations(builder.referencedTypeNamesToAnnotations()); - addReferencedModuleNames(builder.referencedModuleNames()); + if (isElementInfoMutated) { + if (builder.isElementInfoMutated) { + addElementInfo(builder.elementInfo); + } + } else { + elementInfo.clear(); + addElementInfo(builder.elementInfo); + } + if (isOtherElementInfoMutated) { + if (builder.isOtherElementInfoMutated) { + addOtherElementInfo(builder.otherElementInfo); + } + } else { + otherElementInfo.clear(); + addOtherElementInfo(builder.otherElementInfo); + } + addReferencedTypeNamesToAnnotations(builder.referencedTypeNamesToAnnotations); + addReferencedModuleNames(builder.referencedModuleNames); builder.superTypeInfo().ifPresent(this::superTypeInfo); - addInterfaceTypeInfo(builder.interfaceTypeInfo()); - addModifiers(builder.modifiers()); - addElementModifiers(builder.elementModifiers()); + if (isInterfaceTypeInfoMutated) { + if (builder.isInterfaceTypeInfoMutated) { + addInterfaceTypeInfo(builder.interfaceTypeInfo); + } + } else { + interfaceTypeInfo.clear(); + addInterfaceTypeInfo(builder.interfaceTypeInfo); + } + addModifiers(builder.modifiers); + addElementModifiers(builder.elementModifiers); builder.accessModifier().ifPresent(this::accessModifier); builder.module().ifPresent(this::module); builder.originatingElement().ifPresent(this::originatingElement); - addAnnotations(builder.annotations()); - addInheritedAnnotations(builder.inheritedAnnotations()); + if (isAnnotationsMutated) { + if (builder.isAnnotationsMutated) { + addAnnotations(builder.annotations); + } + } else { + annotations.clear(); + addAnnotations(builder.annotations); + } + if (isInheritedAnnotationsMutated) { + if (builder.isInheritedAnnotationsMutated) { + addInheritedAnnotations(builder.inheritedAnnotations); + } + } else { + inheritedAnnotations.clear(); + addInheritedAnnotations(builder.inheritedAnnotations); + } return self(); } @@ -262,6 +317,7 @@ public BUILDER kind(ElementKind kind) { */ public BUILDER elementInfo(List elementInfo) { Objects.requireNonNull(elementInfo); + isElementInfoMutated = true; this.elementInfo.clear(); this.elementInfo.addAll(elementInfo); return self(); @@ -276,6 +332,7 @@ public BUILDER elementInfo(List elementInfo) { */ public BUILDER addElementInfo(List elementInfo) { Objects.requireNonNull(elementInfo); + isElementInfoMutated = true; this.elementInfo.addAll(elementInfo); return self(); } @@ -290,6 +347,7 @@ public BUILDER addElementInfo(List elementInfo) { public BUILDER addElementInfo(TypedElementInfo elementInfo) { Objects.requireNonNull(elementInfo); this.elementInfo.add(elementInfo); + isElementInfoMutated = true; return self(); } @@ -318,6 +376,7 @@ public BUILDER addElementInfo(Consumer consumer) { */ public BUILDER otherElementInfo(List otherElementInfo) { Objects.requireNonNull(otherElementInfo); + isOtherElementInfoMutated = true; this.otherElementInfo.clear(); this.otherElementInfo.addAll(otherElementInfo); return self(); @@ -333,6 +392,7 @@ public BUILDER otherElementInfo(List otherElementInf */ public BUILDER addOtherElementInfo(List otherElementInfo) { Objects.requireNonNull(otherElementInfo); + isOtherElementInfoMutated = true; this.otherElementInfo.addAll(otherElementInfo); return self(); } @@ -348,6 +408,7 @@ public BUILDER addOtherElementInfo(List otherElement public BUILDER addOtherElementInfo(TypedElementInfo otherElementInfo) { Objects.requireNonNull(otherElementInfo); this.otherElementInfo.add(otherElementInfo); + isOtherElementInfoMutated = true; return self(); } @@ -374,8 +435,8 @@ public BUILDER addOtherElementInfo(Consumer consumer) * @return updated builder instance * @see #referencedTypeNamesToAnnotations() */ - public BUILDER referencedTypeNamesToAnnotations(Map> referencedTypeNamesToAnnotations) { + public BUILDER referencedTypeNamesToAnnotations( + Map> referencedTypeNamesToAnnotations) { Objects.requireNonNull(referencedTypeNamesToAnnotations); this.referencedTypeNamesToAnnotations.clear(); this.referencedTypeNamesToAnnotations.putAll(referencedTypeNamesToAnnotations); @@ -539,6 +600,7 @@ public BUILDER superTypeInfo(Consumer consumer) { */ public BUILDER interfaceTypeInfo(List interfaceTypeInfo) { Objects.requireNonNull(interfaceTypeInfo); + isInterfaceTypeInfoMutated = true; this.interfaceTypeInfo.clear(); this.interfaceTypeInfo.addAll(interfaceTypeInfo); return self(); @@ -553,6 +615,7 @@ public BUILDER interfaceTypeInfo(List interfaceTypeInfo) { */ public BUILDER addInterfaceTypeInfo(List interfaceTypeInfo) { Objects.requireNonNull(interfaceTypeInfo); + isInterfaceTypeInfoMutated = true; this.interfaceTypeInfo.addAll(interfaceTypeInfo); return self(); } @@ -567,6 +630,7 @@ public BUILDER addInterfaceTypeInfo(List interfaceTypeInfo) public BUILDER addInterfaceTypeInfo(TypeInfo interfaceTypeInfo) { Objects.requireNonNull(interfaceTypeInfo); this.interfaceTypeInfo.add(interfaceTypeInfo); + isInterfaceTypeInfoMutated = true; return self(); } @@ -744,6 +808,7 @@ public BUILDER originatingElement(Object originatingElement) { */ public BUILDER annotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.clear(); this.annotations.addAll(annotations); return self(); @@ -760,6 +825,7 @@ public BUILDER annotations(List annotations) { */ public BUILDER addAnnotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.addAll(annotations); return self(); } @@ -776,6 +842,7 @@ public BUILDER addAnnotations(List annotations) { public BUILDER addAnnotation(Annotation annotation) { Objects.requireNonNull(annotation); this.annotations.add(annotation); + isAnnotationsMutated = true; return self(); } @@ -802,6 +869,8 @@ public BUILDER addAnnotation(Consumer consumer) { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -809,6 +878,7 @@ public BUILDER addAnnotation(Consumer consumer) { */ public BUILDER inheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.clear(); this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); @@ -820,6 +890,8 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -827,6 +899,7 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati */ public BUILDER addInheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); } @@ -837,6 +910,8 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotation list of all meta annotations of this element * @return updated builder instance @@ -845,6 +920,7 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { Objects.requireNonNull(inheritedAnnotation); this.inheritedAnnotations.add(inheritedAnnotation); + isInheritedAnnotationsMutated = true; return self(); } @@ -854,6 +930,8 @@ public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param consumer list of all meta annotations of this element * @return updated builder instance @@ -1049,6 +1127,8 @@ public List annotations() { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @return the inherited annotations */ diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java b/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java index 4bb3da96300..704c34d139c 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java @@ -16,10 +16,13 @@ package io.helidon.common.types; +import java.util.ArrayDeque; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Queue; import java.util.Set; import io.helidon.builder.api.Option; @@ -228,6 +231,54 @@ default Optional metaAnnotation(TypeName annotation, TypeName metaAn @Option.Redundant Optional originatingElement(); + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypeInfo#typeName()} if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code ClassInfo} when using classpath scanning. + * + * @return originating element, or the type of this type info + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::typeName); + } + + /** + * Checks if the current type implements, or extends the provided type. + * This method analyzes the whole dependency tree of the current type. + * + * @param typeName type of interface to check + * @return the super type info, or interface type info matching the provided type, with appropriate generic declarations + */ + default Optional findInHierarchy(TypeName typeName) { + if (typeName.equals(typeName())) { + return Optional.of((TypeInfo) this); + } + // scan super types + Optional superClass = superTypeInfo(); + if (superClass.isPresent() && !superClass.get().typeName().equals(TypeNames.OBJECT)) { + var superType = superClass.get(); + var foundInSuper = superType.findInHierarchy(typeName); + if (foundInSuper.isPresent()) { + return foundInSuper; + } + } + // nope, let's try interfaces + Queue interfaces = new ArrayDeque<>(interfaceTypeInfo()); + Set processed = new HashSet<>(); + + while (!interfaces.isEmpty()) { + TypeInfo type = interfaces.remove(); + // make sure we process each type only once + if (processed.add(type.typeName())) { + if (typeName.equals(type.typeName())) { + return Optional.of(type); + } + interfaces.addAll(type.interfaceTypeInfo()); + } + } + return Optional.empty(); + } + /** * Uses {@link io.helidon.common.types.TypeInfo#referencedModuleNames()} to determine if the module name is known for the * given type. diff --git a/common/types/src/main/java/io/helidon/common/types/TypeNames.java b/common/types/src/main/java/io/helidon/common/types/TypeNames.java index 62fe59da5a3..f539e5849af 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeNames.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeNames.java @@ -16,8 +16,10 @@ package io.helidon.common.types; +import java.lang.annotation.Documented; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.time.Duration; import java.util.Collection; import java.util.List; @@ -73,10 +75,18 @@ public final class TypeNames { * Type name for {@link java.lang.annotation.Retention}. */ public static final TypeName RETENTION = TypeName.create(Retention.class); + /** + * Type name for {@link java.lang.annotation.Documented}. + */ + public static final TypeName DOCUMENTED = TypeName.create(Documented.class); /** * Type name for {@link java.lang.annotation.Inherited}. */ public static final TypeName INHERITED = TypeName.create(Inherited.class); + /** + * Type name for {@link java.lang.annotation.Target}. + */ + public static final TypeName TARGET = TypeName.create(Target.class); /* Primitive types and their boxed counterparts diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java b/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java index 905b540268f..9560152bd7e 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java @@ -79,7 +79,13 @@ abstract class BuilderBase elementModifiers = new LinkedHashSet<>(); private final Set modifiers = new LinkedHashSet<>(); private AccessModifier accessModifier; + private boolean isAnnotationsMutated; + private boolean isComponentTypesMutated; + private boolean isElementTypeAnnotationsMutated; + private boolean isInheritedAnnotationsMutated; + private boolean isParameterArgumentsMutated; private ElementKind kind; + private ElementSignature signature; private Object originatingElement; private String defaultValue; private String description; @@ -95,7 +101,7 @@ protected BuilderBase() { } /** - * Update this builder from an existing prototype instance. + * Update this builder from an existing prototype instance. This method disables automatic service discovery. * * @param prototype existing prototype to update this builder from * @return updated builder instance @@ -107,16 +113,32 @@ public BUILDER from(TypedElementInfo prototype) { elementTypeKind(prototype.elementTypeKind()); kind(prototype.kind()); defaultValue(prototype.defaultValue()); + if (!isElementTypeAnnotationsMutated) { + elementTypeAnnotations.clear(); + } addElementTypeAnnotations(prototype.elementTypeAnnotations()); + if (!isComponentTypesMutated) { + componentTypes.clear(); + } addComponentTypes(prototype.componentTypes()); addModifiers(prototype.modifiers()); addElementModifiers(prototype.elementModifiers()); accessModifier(prototype.accessModifier()); enclosingType(prototype.enclosingType()); + if (!isParameterArgumentsMutated) { + parameterArguments.clear(); + } addParameterArguments(prototype.parameterArguments()); addThrowsChecked(prototype.throwsChecked()); originatingElement(prototype.originatingElement()); + signature(prototype.signature()); + if (!isAnnotationsMutated) { + annotations.clear(); + } addAnnotations(prototype.annotations()); + if (!isInheritedAnnotationsMutated) { + inheritedAnnotations.clear(); + } addInheritedAnnotations(prototype.inheritedAnnotations()); return self(); } @@ -134,17 +156,53 @@ public BUILDER from(TypedElementInfo.BuilderBase builder) { builder.elementTypeKind().ifPresent(this::elementTypeKind); builder.kind().ifPresent(this::kind); builder.defaultValue().ifPresent(this::defaultValue); - addElementTypeAnnotations(builder.elementTypeAnnotations()); - addComponentTypes(builder.componentTypes()); - addModifiers(builder.modifiers()); - addElementModifiers(builder.elementModifiers()); + if (isElementTypeAnnotationsMutated) { + if (builder.isElementTypeAnnotationsMutated) { + addElementTypeAnnotations(builder.elementTypeAnnotations); + } + } else { + elementTypeAnnotations.clear(); + addElementTypeAnnotations(builder.elementTypeAnnotations); + } + if (isComponentTypesMutated) { + if (builder.isComponentTypesMutated) { + addComponentTypes(builder.componentTypes); + } + } else { + componentTypes.clear(); + addComponentTypes(builder.componentTypes); + } + addModifiers(builder.modifiers); + addElementModifiers(builder.elementModifiers); builder.accessModifier().ifPresent(this::accessModifier); builder.enclosingType().ifPresent(this::enclosingType); - addParameterArguments(builder.parameterArguments()); - addThrowsChecked(builder.throwsChecked()); + if (isParameterArgumentsMutated) { + if (builder.isParameterArgumentsMutated) { + addParameterArguments(builder.parameterArguments); + } + } else { + parameterArguments.clear(); + addParameterArguments(builder.parameterArguments); + } + addThrowsChecked(builder.throwsChecked); builder.originatingElement().ifPresent(this::originatingElement); - addAnnotations(builder.annotations()); - addInheritedAnnotations(builder.inheritedAnnotations()); + builder.signature().ifPresent(this::signature); + if (isAnnotationsMutated) { + if (builder.isAnnotationsMutated) { + addAnnotations(builder.annotations); + } + } else { + annotations.clear(); + addAnnotations(builder.annotations); + } + if (isInheritedAnnotationsMutated) { + if (builder.isInheritedAnnotationsMutated) { + addInheritedAnnotations(builder.inheritedAnnotations); + } + } else { + inheritedAnnotations.clear(); + addInheritedAnnotations(builder.inheritedAnnotations); + } return self(); } @@ -286,7 +344,7 @@ public BUILDER defaultValue(String defaultValue) { } /** - * The list of known annotations on the type name referenced by {@link #typeName()}. + * The list of known annotations on the type name referenced by {@link io.helidon.common.types.TypedElementInfo#typeName()}. * * @param elementTypeAnnotations the list of annotations on this element's (return) type. * @return updated builder instance @@ -294,13 +352,14 @@ public BUILDER defaultValue(String defaultValue) { */ public BUILDER elementTypeAnnotations(List elementTypeAnnotations) { Objects.requireNonNull(elementTypeAnnotations); + isElementTypeAnnotationsMutated = true; this.elementTypeAnnotations.clear(); this.elementTypeAnnotations.addAll(elementTypeAnnotations); return self(); } /** - * The list of known annotations on the type name referenced by {@link #typeName()}. + * The list of known annotations on the type name referenced by {@link io.helidon.common.types.TypedElementInfo#typeName()}. * * @param elementTypeAnnotations the list of annotations on this element's (return) type. * @return updated builder instance @@ -308,6 +367,7 @@ public BUILDER elementTypeAnnotations(List elementTypeAnno */ public BUILDER addElementTypeAnnotations(List elementTypeAnnotations) { Objects.requireNonNull(elementTypeAnnotations); + isElementTypeAnnotationsMutated = true; this.elementTypeAnnotations.addAll(elementTypeAnnotations); return self(); } @@ -321,6 +381,7 @@ public BUILDER addElementTypeAnnotations(List elementTypeA */ public BUILDER componentTypes(List componentTypes) { Objects.requireNonNull(componentTypes); + isComponentTypesMutated = true; this.componentTypes.clear(); this.componentTypes.addAll(componentTypes); return self(); @@ -335,6 +396,7 @@ public BUILDER componentTypes(List componentTypes) { */ public BUILDER addComponentTypes(List componentTypes) { Objects.requireNonNull(componentTypes); + isComponentTypesMutated = true; this.componentTypes.addAll(componentTypes); return self(); } @@ -493,6 +555,7 @@ public BUILDER enclosingType(Consumer consumer) { */ public BUILDER parameterArguments(List parameterArguments) { Objects.requireNonNull(parameterArguments); + isParameterArgumentsMutated = true; this.parameterArguments.clear(); this.parameterArguments.addAll(parameterArguments); return self(); @@ -509,6 +572,7 @@ public BUILDER parameterArguments(List parameterArgu */ public BUILDER addParameterArguments(List parameterArguments) { Objects.requireNonNull(parameterArguments); + isParameterArgumentsMutated = true; this.parameterArguments.addAll(parameterArguments); return self(); } @@ -525,6 +589,7 @@ public BUILDER addParameterArguments(List parameterA public BUILDER addParameterArgument(TypedElementInfo parameterArgument) { Objects.requireNonNull(parameterArgument); this.parameterArguments.add(parameterArgument); + isParameterArgumentsMutated = true; return self(); } @@ -609,6 +674,7 @@ public BUILDER originatingElement(Object originatingElement) { */ public BUILDER annotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.clear(); this.annotations.addAll(annotations); return self(); @@ -625,6 +691,7 @@ public BUILDER annotations(List annotations) { */ public BUILDER addAnnotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.addAll(annotations); return self(); } @@ -641,6 +708,7 @@ public BUILDER addAnnotations(List annotations) { public BUILDER addAnnotation(Annotation annotation) { Objects.requireNonNull(annotation); this.annotations.add(annotation); + isAnnotationsMutated = true; return self(); } @@ -667,6 +735,8 @@ public BUILDER addAnnotation(Consumer consumer) { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -674,6 +744,7 @@ public BUILDER addAnnotation(Consumer consumer) { */ public BUILDER inheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.clear(); this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); @@ -685,6 +756,8 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -692,6 +765,7 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati */ public BUILDER addInheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); } @@ -702,6 +776,8 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotation list of all meta annotations of this element * @return updated builder instance @@ -710,6 +786,7 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { Objects.requireNonNull(inheritedAnnotation); this.inheritedAnnotations.add(inheritedAnnotation); + isInheritedAnnotationsMutated = true; return self(); } @@ -719,6 +796,8 @@ public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param consumer list of all meta annotations of this element * @return updated builder instance @@ -794,7 +873,7 @@ public Optional defaultValue() { } /** - * The list of known annotations on the type name referenced by {@link #typeName()}. + * The list of known annotations on the type name referenced by {@link io.helidon.common.types.TypedElementInfo#typeName()}. * * @return the element type annotations */ @@ -888,6 +967,17 @@ public Optional originatingElement() { return Optional.ofNullable(originatingElement); } + /** + * Signature of this element. + * + * @return the signature + * @see io.helidon.common.types.ElementSignature + * @see #signature() + */ + public Optional signature() { + return Optional.ofNullable(signature); + } + /** * List of declared and known annotations for this element. * Note that "known" implies that the annotation is visible, which depends @@ -905,6 +995,8 @@ public List annotations() { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @return the inherited annotations */ @@ -939,6 +1031,9 @@ protected void validatePrototype() { if (accessModifier == null) { collector.fatal(getClass(), "Property \"accessModifier\" must not be null, but not set"); } + if (signature == null) { + collector.fatal(getClass(), "Property \"signature\" must not be null, but not set"); + } collector.collect().checkValid(); } @@ -999,6 +1094,20 @@ BUILDER originatingElement(Optional originatingElement) { return self(); } + /** + * Signature of this element. + * + * @param signature signature of this element + * @return updated builder instance + * @see io.helidon.common.types.ElementSignature + * @see #signature() + */ + BUILDER signature(ElementSignature signature) { + Objects.requireNonNull(signature); + this.signature = signature; + return self(); + } + /** * Generated implementation of the prototype, can be extended by descendant prototype implementations. */ @@ -1006,6 +1115,7 @@ protected static class TypedElementInfoImpl implements TypedElementInfo { private final AccessModifier accessModifier; private final ElementKind kind; + private final ElementSignature signature; private final List annotations; private final List elementTypeAnnotations; private final List inheritedAnnotations; @@ -1043,6 +1153,7 @@ protected TypedElementInfoImpl(TypedElementInfo.BuilderBase builder) { this.parameterArguments = List.copyOf(builder.parameterArguments()); this.throwsChecked = Collections.unmodifiableSet(new LinkedHashSet<>(builder.throwsChecked())); this.originatingElement = builder.originatingElement(); + this.signature = builder.signature().get(); this.annotations = List.copyOf(builder.annotations()); this.inheritedAnnotations = List.copyOf(builder.inheritedAnnotations()); } @@ -1132,6 +1243,11 @@ public Optional originatingElement() { return originatingElement; } + @Override + public ElementSignature signature() { + return signature; + } + @Override public List annotations() { return annotations; @@ -1156,6 +1272,7 @@ public boolean equals(Object o) { && Objects.equals(enclosingType, other.enclosingType()) && Objects.equals(parameterArguments, other.parameterArguments()) && Objects.equals(throwsChecked, other.throwsChecked()) + && Objects.equals(signature, other.signature()) && Objects.equals(annotations, other.annotations()) && Objects.equals(inheritedAnnotations, other.inheritedAnnotations()); } @@ -1168,6 +1285,7 @@ public int hashCode() { enclosingType, parameterArguments, throwsChecked, + signature, annotations, inheritedAnnotations); } diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java index b52ba88a4bd..3de312eb13c 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java @@ -167,4 +167,25 @@ interface TypedElementInfoBlueprint extends Annotated { */ @Option.Redundant Optional originatingElement(); + + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypedElementInfo#signature()} + * if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code MethodInfo} (and such) when using classpath scanning. + * + * @return originating element, or the signature of this element + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::signature); + } + + /** + * Signature of this element. + * + * @return signature of this element + * @see io.helidon.common.types.ElementSignature + */ + @Option.Access("") + ElementSignature signature(); } diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java index 9edf23abd11..0cba7edaff8 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.helidon.common.types; +import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; @@ -60,10 +61,67 @@ static class BuilderDecorator implements Prototype.BuilderDecorator target) { -/* + backwardCompatibility(target); + constructorName(target); + signature(target); + } + + private void signature(TypedElementInfo.BuilderBase target) { + if (target.kind().isEmpty()) { + // this will fail when validating + target.signature(ElementSignatures.createNone()); + return; + } else { + target.signature(signature(target, target.kind().get())); + } + } + + private ElementSignature signature(TypedElementInfo.BuilderBase target, ElementKind elementKind) { + if (elementKind == ElementKind.CONSTRUCTOR) { + return ElementSignatures.createConstructor(toTypes(target.parameterArguments())); + } + // for everything else we need the type (it is required) + if (target.typeName().isEmpty() || target.elementName().isEmpty()) { + return ElementSignatures.createNone(); + } + + TypeName typeName = target.typeName().get(); + String name = target.elementName().get(); + + if (elementKind == ElementKind.FIELD + || elementKind == ElementKind.RECORD_COMPONENT + || elementKind == ElementKind.ENUM_CONSTANT) { + return ElementSignatures.createField(typeName, name); + } + if (elementKind == ElementKind.METHOD) { + return ElementSignatures.createMethod(typeName, name, toTypes(target.parameterArguments())); + } + if (elementKind == ElementKind.PARAMETER) { + return ElementSignatures.createParameter(typeName, name); + } + return ElementSignatures.createNone(); + } + + private List toTypes(List typedElementInfos) { + return typedElementInfos.stream() + .map(TypedElementInfo::typeName) + .collect(Collectors.toUnmodifiableList()); + } + + private void constructorName(TypedElementInfo.BuilderBase target) { + Optional elementKind = target.kind(); + if (elementKind.isPresent()) { + if (elementKind.get() == ElementKind.CONSTRUCTOR) { + target.elementName(""); + } + } + } + + @SuppressWarnings("removal") + private void backwardCompatibility(TypedElementInfo.BuilderBase target) { + /* Backward compatibility for deprecated methods. */ if (target.kind().isEmpty() && target.elementTypeKind().isPresent()) { @@ -103,14 +161,6 @@ public void decorate(TypedElementInfo.BuilderBase target) { target.addModifier(typeModifier.modifierName()); } target.addModifier(target.accessModifier().get().modifierName()); - - - Optional elementKind = target.kind(); - if (elementKind.isPresent()) { - if (elementKind.get() == ElementKind.CONSTRUCTOR) { - target.elementName(""); - } - } } } } diff --git a/common/types/src/test/java/io/helidon/common/types/SignatureTest.java b/common/types/src/test/java/io/helidon/common/types/SignatureTest.java new file mode 100644 index 00000000000..2dce94997b9 --- /dev/null +++ b/common/types/src/test/java/io/helidon/common/types/SignatureTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +class SignatureTest { + + @Test + void testMethodSignature() { + TypedElementInfo m1 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m2 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m3 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method2") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m4 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method") + .typeName(TypeNames.STRING) + .parameterArguments(List.of(TypedElementInfo.builder() + .kind(ElementKind.PARAMETER) + .typeName(TypeNames.STRING) + .elementName("param1") + .buildPrototype())) + .build(); + + ElementSignature s1 = m1.signature(); + ElementSignature s2 = m2.signature(); + ElementSignature s3 = m3.signature(); + ElementSignature s4 = m4.signature(); + + // this is specified in Javadoc and must not be changed + assertThat(s1.text(), is("method()")); + assertThat(s2.text(), is("method()")); + assertThat(s3.text(), is("method2()")); + assertThat(s4.text(), is("method(java.lang.String)")); + + assertThat(s1, is(s2)); + assertThat(s1, not(s3)); + assertThat(s1, not(s4)); + + assertThat(s1.hashCode(), is(s2.hashCode())); + + assertThat(s1.name(), is("method")); + assertThat(s1.type(), is(TypeNames.STRING)); + assertThat(s1.parameterTypes(), is(List.of())); + } + + @Test + void testConstructorSignature() { + TypedElementInfo m1 = TypedElementInfo.builder() + .kind(ElementKind.CONSTRUCTOR) + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m2 = TypedElementInfo.builder() + .kind(ElementKind.CONSTRUCTOR) + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m3 = TypedElementInfo.builder() + .kind(ElementKind.CONSTRUCTOR) + .typeName(TypeNames.STRING) + .parameterArguments(List.of(TypedElementInfo.builder() + .kind(ElementKind.PARAMETER) + .typeName(TypeNames.STRING) + .elementName("param1") + .buildPrototype())) + .build(); + + ElementSignature s1 = m1.signature(); + ElementSignature s2 = m2.signature(); + ElementSignature s3 = m3.signature(); + + // this is specified in Javadoc and must not be changed + assertThat(s1.text(), is("()")); + assertThat(s2.text(), is("()")); + assertThat(s3.text(), is("(java.lang.String)")); + + assertThat(s1, is(s2)); + assertThat(s1, not(s3)); + + assertThat(s1.hashCode(), is(s2.hashCode())); + + assertThat(s1.name(), is("")); + assertThat(s1.type(), is(TypeNames.PRIMITIVE_VOID)); + assertThat(s1.parameterTypes(), is(List.of())); + } + + @Test + void testFieldSignature() { + TypedElementInfo m1 = TypedElementInfo.builder() + .kind(ElementKind.FIELD) + .elementName("field") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m2 = TypedElementInfo.builder() + .kind(ElementKind.FIELD) + .elementName("field") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m3 = TypedElementInfo.builder() + .kind(ElementKind.FIELD) + .elementName("field2") + .typeName(TypeNames.STRING) + .build(); + + ElementSignature s1 = m1.signature(); + ElementSignature s2 = m2.signature(); + ElementSignature s3 = m3.signature(); + + // this is specified in Javadoc and must not be changed + assertThat(s1.text(), is("field")); + assertThat(s2.text(), is("field")); + assertThat(s3.text(), is("field2")); + + assertThat(s1, is(s2)); + assertThat(s1, not(s3)); + + assertThat(s1.hashCode(), is(s2.hashCode())); + + assertThat(s1.name(), is("field")); + assertThat(s1.type(), is(TypeNames.STRING)); + assertThat(s1.parameterTypes(), is(List.of())); + } +} diff --git a/common/types/src/test/java/io/helidon/common/types/TypeInfoTest.java b/common/types/src/test/java/io/helidon/common/types/TypeInfoTest.java new file mode 100644 index 00000000000..f00f182dc2b --- /dev/null +++ b/common/types/src/test/java/io/helidon/common/types/TypeInfoTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.types; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +class TypeInfoTest { + @Test + void testFindInHierarchyInterfaces() { + TypeName ifaceA = TypeName.create("io.helidon.common.types.test.A"); + TypeName ifaceB = TypeName.create("io.helidon.common.types.test.B"); + TypeName ifaceC = TypeName.create("io.helidon.common.types.test.C"); + TypeInfo aInfo = TypeInfo.builder() + .typeName(ifaceA) + .kind(ElementKind.INTERFACE) + .build(); + TypeInfo bInfo = TypeInfo.builder() + .typeName(ifaceB) + .kind(ElementKind.INTERFACE) + .addInterfaceTypeInfo(aInfo) + .build(); + TypeInfo cInfo = TypeInfo.builder() + .typeName(ifaceC) + .kind(ElementKind.INTERFACE) + .addInterfaceTypeInfo(bInfo) + .build(); + + Optional foundInfo = cInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = cInfo.findInHierarchy(ifaceB); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(bInfo)); + + foundInfo = bInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = aInfo.findInHierarchy(ifaceB); + assertThat(foundInfo, is(Optional.empty())); + } + + @Test + void testFindInHierarchyTypes() { + TypeName ifaceA = TypeName.create("io.helidon.common.types.test.A"); + TypeName classB = TypeName.create("io.helidon.common.types.test.B"); + TypeName classC = TypeName.create("io.helidon.common.types.test.C"); + TypeInfo aInfo = TypeInfo.builder() + .typeName(ifaceA) + .kind(ElementKind.INTERFACE) + .build(); + TypeInfo bInfo = TypeInfo.builder() + .typeName(classB) + .kind(ElementKind.CLASS) + .addInterfaceTypeInfo(aInfo) + .build(); + TypeInfo cInfo = TypeInfo.builder() + .typeName(classC) + .kind(ElementKind.INTERFACE) + .superTypeInfo(bInfo) + .build(); + + Optional foundInfo = cInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = cInfo.findInHierarchy(classB); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(bInfo)); + + foundInfo = bInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = aInfo.findInHierarchy(classB); + assertThat(foundInfo, is(Optional.empty())); + } + +} diff --git a/common/uri/pom.xml b/common/uri/pom.xml index 23a1dd61bf3..48eea6dc12a 100644 --- a/common/uri/pom.xml +++ b/common/uri/pom.xml @@ -23,7 +23,7 @@ io.helidon.common helidon-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-common-uri Helidon Common URI diff --git a/config/config-mp/pom.xml b/config/config-mp/pom.xml index 1fa5e301a2e..a183efb428e 100644 --- a/config/config-mp/pom.xml +++ b/config/config-mp/pom.xml @@ -22,7 +22,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-mp diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java index 8299a1c1cc5..d35fdb91b16 100644 --- a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java @@ -30,6 +30,7 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; @@ -452,7 +453,11 @@ private Optional> findImplicit(Class type) { if (Enum.class.isAssignableFrom(type)) { return Optional.of(value -> { Class enumClass = (Class) type; - return (T) Enum.valueOf(enumClass, value); + try { + return (T) Enum.valueOf(enumClass, value); + } catch (Exception e) { + return (T) Enum.valueOf(enumClass, value.toUpperCase(Locale.ROOT)); + } }); } // any class that has a "public static T method()" diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java index 9ecd670227d..4b6f57efdc1 100644 --- a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import java.util.stream.Stream; import io.helidon.common.GenericType; +import io.helidon.common.config.GlobalConfig; import io.helidon.config.ConfigValue; import io.helidon.config.MetaConfig; import io.helidon.config.spi.ConfigMapper; @@ -167,8 +168,8 @@ public static void buildTimeEnd() { private ConfigDelegate doRegisterConfig(Config config, ClassLoader classLoader) { ConfigDelegate currentConfig = CONFIGS.remove(classLoader); - if (config instanceof ConfigDelegate) { - config = ((ConfigDelegate) config).delegate(); + if (config instanceof ConfigDelegate delegate) { + config = delegate.delegate(); } if (null != currentConfig) { @@ -178,6 +179,11 @@ private ConfigDelegate doRegisterConfig(Config config, ClassLoader classLoader) ConfigDelegate newConfig = new ConfigDelegate(config); CONFIGS.put(classLoader, newConfig); + if (classLoader == Thread.currentThread().getContextClassLoader()) { + // this should be the default class loader (we do not support classloader magic in Helidon) + GlobalConfig.config(() -> newConfig, true); + } + return newConfig; } diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java b/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java index 514830dab44..5ec1c8d3e43 100644 --- a/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,8 +84,8 @@ class SeConfig implements Config { this.stringKey = prefix.child(key).toString(); this.stringPrefix = prefix.toString(); - if (delegate instanceof MpConfigImpl) { - this.delegateImpl = (MpConfigImpl) delegate; + if (delegate instanceof MpConfigImpl mpConfig) { + this.delegateImpl = mpConfig; } else { this.delegateImpl = null; } diff --git a/config/config/pom.xml b/config/config/pom.xml index 374b0294508..256ca780600 100644 --- a/config/config/pom.xml +++ b/config/config/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config Helidon Config diff --git a/config/config/src/main/java/io/helidon/config/BuilderImpl.java b/config/config/src/main/java/io/helidon/config/BuilderImpl.java index 28e60577289..a1bd6f09681 100644 --- a/config/config/src/main/java/io/helidon/config/BuilderImpl.java +++ b/config/config/src/main/java/io/helidon/config/BuilderImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023 Oracle and/or its affiliates. + * Copyright (c) 2017, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,6 +61,7 @@ class BuilderImpl implements Config.Builder { private MergingStrategy mergingStrategy = MergingStrategy.fallback(); private boolean hasSystemPropertiesSource; private boolean hasEnvVarSource; + private boolean sourcesConfigured; /* * Config mapper providers */ @@ -123,6 +124,8 @@ public Config.Builder sources(List> sourceSuppl sourceSuppliers.stream() .map(Supplier::get) .forEach(this::addSource); + // this was intentional, even if empty (such as from Config.just()) + this.sourcesConfigured = true; return this; } @@ -427,14 +430,14 @@ private ConfigSourcesRuntime buildConfigSources(ConfigContextImpl context) { envVarAliasGeneratorEnabled = true; } - boolean nothingConfigured = sources.isEmpty(); + boolean nothingConfigured = sources.isEmpty() && !sourcesConfigured; if (nothingConfigured) { // use meta configuration to load all sources - MetaConfig.configSources(mediaType -> context.findParser(mediaType).isPresent(), context.supportedSuffixes()) - .stream() + MetaConfigFinder.findConfigSource(mediaType -> context.findParser(mediaType).isPresent(), + context.supportedSuffixes()) .map(context::sourceRuntimeBase) - .forEach(targetSources::add); + .ifPresent(targetSources::add); } else { // add all configured or discovered sources @@ -702,56 +705,6 @@ public String toString() { } } - private static final class WeightedConfigSource implements Weighted { - private final HelidonSourceWithPriority source; - private final ConfigContext context; - - private WeightedConfigSource(HelidonSourceWithPriority source, ConfigContext context) { - this.source = source; - this.context = context; - } - - @Override - public double weight() { - return source.weight(context); - } - - private ConfigSourceRuntimeImpl runtime(ConfigContextImpl context) { - return context.sourceRuntimeBase(source.unwrap()); - } - } - - private static final class HelidonSourceWithPriority { - private final ConfigSource configSource; - private final Double explicitWeight; - - private HelidonSourceWithPriority(ConfigSource configSource, Double explicitWeight) { - this.configSource = configSource; - this.explicitWeight = explicitWeight; - } - - ConfigSource unwrap() { - return configSource; - } - - double weight(ConfigContext context) { - // first - explicit priority. If configured by user, return it - if (null != explicitWeight) { - return explicitWeight; - } - - // ordinal from data - return context.sourceRuntime(configSource) - .node("config_priority") - .flatMap(node -> node.value() - .map(Double::parseDouble)) - .orElseGet(() -> { - // the config source does not have an ordinal configured, I need to get it from other places - return Weights.find(configSource, Weighted.DEFAULT_WEIGHT); - }); - } - } - private static class LoadedFilterProvider implements Function { private final ConfigFilter filter; diff --git a/config/config/src/main/java/io/helidon/config/Config.java b/config/config/src/main/java/io/helidon/config/Config.java index f34c4a88d38..bf59b50f5c3 100644 --- a/config/config/src/main/java/io/helidon/config/Config.java +++ b/config/config/src/main/java/io/helidon/config/Config.java @@ -1635,8 +1635,14 @@ default Builder sources(Supplier configSource, * @see #config(Config) */ default Builder metaConfig() { - MetaConfig.metaConfig() - .ifPresent(this::config); + try { + MetaConfig.metaConfig() + .ifPresent(this::config); + } catch (Exception e) { + System.getLogger(getClass().getName()) + .log(System.Logger.Level.WARNING, "Failed to load SE meta-configuration," + + " please make sure it has correct format.", e); + } return this; } diff --git a/config/config/src/main/java/io/helidon/config/ConfigDiff.java b/config/config/src/main/java/io/helidon/config/ConfigDiff.java index 13126dbf629..156c5652121 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigDiff.java +++ b/config/config/src/main/java/io/helidon/config/ConfigDiff.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020 Oracle and/or its affiliates. + * Copyright (c) 2017, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,8 +86,8 @@ private static boolean notEqual(Config left, Config right) { } private static Optional value(Config node) { - if (node instanceof AbstractConfigImpl) { - return ((AbstractConfigImpl) node).value(); + if (node instanceof AbstractConfigImpl abstractConfig) { + return abstractConfig.value(); } return node.asString().asOptional(); } diff --git a/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java b/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java index f920e4f8883..30e36d84dd2 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java +++ b/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -109,8 +109,8 @@ ConfigKeyImpl child(String key) { @Override public ConfigKeyImpl child(io.helidon.common.config.Config.Key key) { final List path; - if (key instanceof ConfigKeyImpl) { - path = ((ConfigKeyImpl) key).path; + if (key instanceof ConfigKeyImpl configKey) { + path = configKey.path; } else { path = new LinkedList<>(); while (!key.isRoot()) { diff --git a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java b/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java index 7c91359737f..f95c91bfbeb 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java +++ b/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,16 +83,15 @@ class ConfigSourceRuntimeImpl implements ConfigSourceRuntime { // content source AtomicReference lastStamp = new AtomicReference<>(); - if (configSource instanceof ParsableSource) { + if (configSource instanceof ParsableSource parsableSource) { // eager parsable config source - reloader = new ParsableConfigSourceReloader(configContext, (ParsableSource) source, lastStamp); + reloader = new ParsableConfigSourceReloader(configContext, parsableSource, lastStamp); singleNodeFunction = objectNodeToSingleNode(); - } else if (configSource instanceof NodeConfigSource) { + } else if (configSource instanceof NodeConfigSource nodeConfigSource) { // eager node config source - reloader = new NodeConfigSourceReloader((NodeConfigSource) source, lastStamp); + reloader = new NodeConfigSourceReloader(nodeConfigSource, lastStamp); singleNodeFunction = objectNodeToSingleNode(); - } else if (configSource instanceof LazyConfigSource) { - LazyConfigSource lazySource = (LazyConfigSource) source; + } else if (configSource instanceof LazyConfigSource lazySource) { // lazy config source reloader = Optional::empty; singleNodeFunction = lazySource::node; @@ -143,8 +142,7 @@ class ConfigSourceRuntimeImpl implements ConfigSourceRuntime { } } - if (!changesSupported && (configSource instanceof EventConfigSource)) { - EventConfigSource event = (EventConfigSource) source; + if (!changesSupported && (configSource instanceof EventConfigSource event)) { changesSupported = true; changesRunnable = () -> event.onChange((key, config) -> listeners.forEach(it -> it.accept(key, config))); } @@ -222,8 +220,8 @@ private synchronized void initialLoad() { } // we may have media type mapping per node configured as well - if (configSource instanceof AbstractConfigSource) { - loadedData = loadedData.map(it -> ((AbstractConfigSource) configSource) + if (configSource instanceof AbstractConfigSource abstractConfigSource) { + loadedData = loadedData.map(it -> abstractConfigSource .processNodeMapping(configContext::findParser, ConfigKeyImpl.of(), it)); } diff --git a/config/config/src/main/java/io/helidon/config/MetaConfig.java b/config/config/src/main/java/io/helidon/config/MetaConfig.java index afbe1b9c502..f78a7df2edb 100644 --- a/config/config/src/main/java/io/helidon/config/MetaConfig.java +++ b/config/config/src/main/java/io/helidon/config/MetaConfig.java @@ -22,7 +22,6 @@ import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; -import java.util.function.Function; import io.helidon.common.HelidonServiceLoader; import io.helidon.common.media.type.MediaType; @@ -241,18 +240,6 @@ static List configSources(Config metaConfig) { return configSources; } - // only interested in config source - static List configSources(Function supportedMediaType, List supportedSuffixes) { - Optional metaConfigOpt = metaConfig(); - - return metaConfigOpt - .map(MetaConfig::configSources) - .orElseGet(() -> MetaConfigFinder.findConfigSource(supportedMediaType, supportedSuffixes) - .map(List::of) - .orElseGet(List::of)); - - } - private static Config createDefault() { // use defaults Config.Builder builder = Config.builder(); diff --git a/config/config/src/main/java/io/helidon/config/UrlConfigSource.java b/config/config/src/main/java/io/helidon/config/UrlConfigSource.java index 24ebeda0c94..c936442e014 100644 --- a/config/config/src/main/java/io/helidon/config/UrlConfigSource.java +++ b/config/config/src/main/java/io/helidon/config/UrlConfigSource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -139,8 +139,8 @@ public Optional load() throws ConfigException { try { URLConnection urlConnection = url.openConnection(); - if (urlConnection instanceof HttpURLConnection) { - return httpContent((HttpURLConnection) urlConnection); + if (urlConnection instanceof HttpURLConnection httpURLConnection) { + return httpContent(httpURLConnection); } else { return genericContent(urlConnection); } diff --git a/config/config/src/main/java/io/helidon/config/UrlHelper.java b/config/config/src/main/java/io/helidon/config/UrlHelper.java index 1267390e555..673c4564a32 100644 --- a/config/config/src/main/java/io/helidon/config/UrlHelper.java +++ b/config/config/src/main/java/io/helidon/config/UrlHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,8 +44,7 @@ static Optional dataStamp(URL url) { // the URL may not be an HTTP URL try { URLConnection urlConnection = url.openConnection(); - if (urlConnection instanceof HttpURLConnection) { - HttpURLConnection connection = (HttpURLConnection) urlConnection; + if (urlConnection instanceof HttpURLConnection connection) { try { connection.setRequestMethod(HEAD_METHOD); if (STATUS_NOT_FOUND == connection.getResponseCode()) { diff --git a/config/config/src/main/resources/META-INF/helidon/service.loader b/config/config/src/main/resources/META-INF/helidon/service.loader index d07172ed9b3..c951c168384 100644 --- a/config/config/src/main/resources/META-INF/helidon/service.loader +++ b/config/config/src/main/resources/META-INF/helidon/service.loader @@ -1,4 +1,6 @@ # List of service contracts we want to support either from service registry, or from service loader io.helidon.config.spi.ConfigParser io.helidon.config.spi.ConfigFilter -io.helidon.config.spi.ConfigMapperProvider +# This cannot be done for now, as ObjectConfigMapper ends up before built-ins when +# we disable mapper services +# io.helidon.config.spi.ConfigMapperProvider diff --git a/config/encryption/pom.xml b/config/encryption/pom.xml index 6e6fb37e978..5e138da452a 100644 --- a/config/encryption/pom.xml +++ b/config/encryption/pom.xml @@ -23,7 +23,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 helidon-config-encryption diff --git a/config/etcd/pom.xml b/config/etcd/pom.xml index bf74074c3b9..64ef0b66e28 100644 --- a/config/etcd/pom.xml +++ b/config/etcd/pom.xml @@ -23,7 +23,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-etcd Helidon Config Etcd diff --git a/config/git/pom.xml b/config/git/pom.xml index 6d853422666..4e16be92b40 100644 --- a/config/git/pom.xml +++ b/config/git/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-git Helidon Config Git diff --git a/config/hocon-mp/pom.xml b/config/hocon-mp/pom.xml index 4afccf75e47..ed3e599d861 100644 --- a/config/hocon-mp/pom.xml +++ b/config/hocon-mp/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-hocon-mp Helidon Config HOCON MP diff --git a/config/hocon/pom.xml b/config/hocon/pom.xml index 203ac5e7135..6349198366b 100644 --- a/config/hocon/pom.xml +++ b/config/hocon/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-hocon Helidon Config HOCON diff --git a/config/metadata/codegen/pom.xml b/config/metadata/codegen/pom.xml index c71e27a4180..d12cb4538b2 100644 --- a/config/metadata/codegen/pom.xml +++ b/config/metadata/codegen/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-metadata-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.config.metadata diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java index 482d7bbb3d0..47e4bf2f96b 100644 --- a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java @@ -109,10 +109,12 @@ private void storeMetadata() { .build()); } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (PrintWriter w = new PrintWriter(baos, true, StandardCharsets.UTF_8)) { - Hson.Array.create(root).write(w); + if (!root.isEmpty()) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PrintWriter w = new PrintWriter(baos, true, StandardCharsets.UTF_8)) { + Hson.Array.create(root).write(w); + } + ctx.filer().writeResource(baos.toByteArray(), META_FILE); } - ctx.filer().writeResource(baos.toByteArray(), META_FILE); } } diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java index 11b94b43380..afc3b65a1fa 100644 --- a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java @@ -157,14 +157,14 @@ private OptionType typeForBlueprintFromSignature(TypedElementInfo element, if (!ElementInfoPredicates.hasNoArgs(element)) { throw new CodegenException("Method " + element + " is annotated with @Configured, " + "yet it has a parameter. Interface methods must not have parameters.", - element.originatingElement().orElse(element.elementName())); + element.originatingElementValue()); } TypeName returnType = element.typeName(); if (ElementInfoPredicates.isVoid(element)) { throw new CodegenException("Method " + element + " is annotated with @Configured, " + "yet it is void. Interface methods must return the property type.", - element.originatingElement().orElse(element.elementName())); + element.originatingElementValue()); } if (returnType.isOptional()) { diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerMetaApi.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerMetaApi.java index aaf0587ae7c..70b1ef5ce7c 100644 --- a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerMetaApi.java +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerMetaApi.java @@ -217,7 +217,7 @@ private void processTargetType(TypeInfo typeInfo, ConfiguredType type, TypeName throw new CodegenException("Type " + typeName.fqName() + " is marked with @Configured" + ", yet it has a static builder() method. Please mark the builder instead " + "of this class.", - typeInfo.originatingElement().orElseGet(typeInfo::typeName)); + typeInfo.originatingElementValue()); } } @@ -243,14 +243,14 @@ private void processTargetType(TypeInfo typeInfo, ConfiguredType type, TypeName + validMethod + " does not have value defined. It is mandatory on non-builder " + "methods", - typeInfo.originatingElement().orElseGet(typeInfo::typeName)); + typeInfo.originatingElementValue()); } if (data.description() == null || data.description().isBlank()) { throw new CodegenException("ConfiguredOption on " + typeName.fqName() + "." + validMethod + " does not have description defined. It is mandatory on non-builder " + "methods", - typeInfo.originatingElement().orElseGet(typeInfo::typeName)); + typeInfo.originatingElementValue()); } if (data.type() == null) { @@ -281,7 +281,7 @@ private void processTargetType(TypeInfo typeInfo, ConfiguredType type, TypeName throw new CodegenException("Type " + typeName.fqName() + " is marked as standalone configuration unit, " + "yet it does have " + "neither a builder method, nor a create method", - typeInfo.originatingElement().orElseGet(typeInfo::typeName)); + typeInfo.originatingElementValue()); } typeInfo.elementInfo() @@ -342,7 +342,7 @@ private OptionType optionType(TypedElementInfo elementInfo, ConfiguredOptionData throw new CodegenException("Method " + elementInfo.elementName() + " is annotated with @ConfiguredOption, " + "yet it does not have explicit type, or exactly one parameter", - typeInfo.originatingElement().orElseGet(typeInfo::typeName)); + typeInfo.originatingElementValue()); } else { TypedElementInfo parameter = parameters.iterator().next(); TypeName paramType = parameter.typeName(); diff --git a/config/metadata/docs/pom.xml b/config/metadata/docs/pom.xml index 651f9e42b0d..d13301dc882 100644 --- a/config/metadata/docs/pom.xml +++ b/config/metadata/docs/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-metadata-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.config.metadata diff --git a/config/metadata/metadata/pom.xml b/config/metadata/metadata/pom.xml index b9873a3b60b..701d5fe8751 100644 --- a/config/metadata/metadata/pom.xml +++ b/config/metadata/metadata/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-metadata-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-metadata Helidon Config Metadata diff --git a/config/metadata/pom.xml b/config/metadata/pom.xml index 33d2fd5afde..6801c248352 100644 --- a/config/metadata/pom.xml +++ b/config/metadata/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-metadata-project diff --git a/config/metadata/processor/pom.xml b/config/metadata/processor/pom.xml index 6361f3e326f..a9162017b43 100644 --- a/config/metadata/processor/pom.xml +++ b/config/metadata/processor/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-metadata-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-metadata-processor Helidon Config Metadata Annotation Processor diff --git a/config/object-mapping/pom.xml b/config/object-mapping/pom.xml index bc6d6a13034..32f50b4709b 100644 --- a/config/object-mapping/pom.xml +++ b/config/object-mapping/pom.xml @@ -22,7 +22,7 @@ helidon-config-project io.helidon.config - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/config/pom.xml b/config/pom.xml index d923bc1f73c..13eded0619c 100644 --- a/config/pom.xml +++ b/config/pom.xml @@ -24,7 +24,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.config helidon-config-project diff --git a/config/testing/pom.xml b/config/testing/pom.xml index 85f1c126f9a..d5c3c46b491 100644 --- a/config/testing/pom.xml +++ b/config/testing/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-testing Helidon Config Testing diff --git a/config/tests/config-metadata-builder-api/pom.xml b/config/tests/config-metadata-builder-api/pom.xml index e7e30d8fdec..cfa46887a48 100644 --- a/config/tests/config-metadata-builder-api/pom.xml +++ b/config/tests/config-metadata-builder-api/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-config-metadata-builder-api Helidon Config Tests Config Meta-Data Meta API diff --git a/config/tests/config-metadata-meta-api/pom.xml b/config/tests/config-metadata-meta-api/pom.xml index 8a3d94e50a7..f78c210c8ff 100644 --- a/config/tests/config-metadata-meta-api/pom.xml +++ b/config/tests/config-metadata-meta-api/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-config-metadata-meta-api Helidon Config Tests Config Meta-Data Meta API diff --git a/config/tests/integration-tests/pom.xml b/config/tests/integration-tests/pom.xml index f6262cad1f7..4cfbf1edc6b 100644 --- a/config/tests/integration-tests/pom.xml +++ b/config/tests/integration-tests/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-integration-tests Helidon Config Tests Integration diff --git a/config/tests/module-mappers-1-base/pom.xml b/config/tests/module-mappers-1-base/pom.xml index d5d53bd582c..b1187a415fb 100644 --- a/config/tests/module-mappers-1-base/pom.xml +++ b/config/tests/module-mappers-1-base/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-module-mappers-1-base Helidon Config Tests Mappers 1 diff --git a/config/tests/module-mappers-2-override/pom.xml b/config/tests/module-mappers-2-override/pom.xml index e1657e603bd..6f7c64c8ab4 100644 --- a/config/tests/module-mappers-2-override/pom.xml +++ b/config/tests/module-mappers-2-override/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-module-mappers-2-override Helidon Config Tests Parser 2 diff --git a/config/tests/module-meta-source-1/pom.xml b/config/tests/module-meta-source-1/pom.xml index aebbcd9e4be..175faca61d7 100644 --- a/config/tests/module-meta-source-1/pom.xml +++ b/config/tests/module-meta-source-1/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-module-meta-source-1 Helidon Config Tests Meta Source 1 diff --git a/config/tests/module-meta-source-2/pom.xml b/config/tests/module-meta-source-2/pom.xml index 39105919760..796d5dd73c4 100644 --- a/config/tests/module-meta-source-2/pom.xml +++ b/config/tests/module-meta-source-2/pom.xml @@ -25,7 +25,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-module-meta-source-2 Helidon Config Tests Meta Source 2 diff --git a/config/tests/module-parsers-1-override/pom.xml b/config/tests/module-parsers-1-override/pom.xml index 4157f6cdc50..3c50a2be415 100644 --- a/config/tests/module-parsers-1-override/pom.xml +++ b/config/tests/module-parsers-1-override/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-module-parsers-1-override Helidon Config Tests Parser 1 diff --git a/config/tests/pom.xml b/config/tests/pom.xml index 28d007e3c1f..66c7c912e19 100644 --- a/config/tests/pom.xml +++ b/config/tests/pom.xml @@ -23,7 +23,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.config.tests helidon-config-tests-project @@ -64,5 +64,8 @@ config-metadata-meta-api config-metadata-builder-api test-lazy-source + test-no-config-sources + test-default-config-source + test-mp-se-meta diff --git a/config/tests/service-registry/pom.xml b/config/tests/service-registry/pom.xml index 05062aa34d2..6da35b58e99 100644 --- a/config/tests/service-registry/pom.xml +++ b/config/tests/service-registry/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-service-registry Helidon Config Tests Service Registry diff --git a/config/tests/test-bundle/pom.xml b/config/tests/test-bundle/pom.xml index e4599c5cbdc..9e1031be7cb 100644 --- a/config/tests/test-bundle/pom.xml +++ b/config/tests/test-bundle/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-bundle Helidon Config Tests Bundle diff --git a/config/tests/test-default-config-source/pom.xml b/config/tests/test-default-config-source/pom.xml new file mode 100644 index 00000000000..096c78691f4 --- /dev/null +++ b/config/tests/test-default-config-source/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + io.helidon.config.tests + helidon-config-tests-project + 4.2.0-SNAPSHOT + ../pom.xml + + helidon-test-default-config-source + Helidon Config Tests Default Config Source + + + Test that when no config sources are configured, we do not fall to meta config, but we want to + use default config sources + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + diff --git a/config/tests/test-default-config-source/src/main/resources/application.yaml b/config/tests/test-default-config-source/src/main/resources/application.yaml new file mode 100644 index 00000000000..cf3e2c3af54 --- /dev/null +++ b/config/tests/test-default-config-source/src/main/resources/application.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +value: "from-file" diff --git a/config/tests/test-default-config-source/src/main/resources/meta-config.yaml b/config/tests/test-default-config-source/src/main/resources/meta-config.yaml new file mode 100644 index 00000000000..01468e56507 --- /dev/null +++ b/config/tests/test-default-config-source/src/main/resources/meta-config.yaml @@ -0,0 +1,21 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# MP style, which would normally fail in Helidon SE, but should be ignored, as we do not specify meta config +add-default-sources: true +sources: + - type: "properties" + classpath: "app.properties" \ No newline at end of file diff --git a/config/tests/test-default-config-source/src/test/java/io/helidon/config/tests/nosources/DefaultSourceTest.java b/config/tests/test-default-config-source/src/test/java/io/helidon/config/tests/nosources/DefaultSourceTest.java new file mode 100644 index 00000000000..6775f4a3211 --- /dev/null +++ b/config/tests/test-default-config-source/src/test/java/io/helidon/config/tests/nosources/DefaultSourceTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.tests.nosources; + +import java.util.Optional; + +import io.helidon.config.Config; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class DefaultSourceTest { + @Test + public void testDefaultSource() { + Config config = Config.builder() + .disableSystemPropertiesSource() + .disableEnvironmentVariablesSource() + .build(); + + Optional value = config.get("value") + .asString() + .asOptional(); + + // meta config MUST be ignored + assertThat("We defined not sources, we should fall back to default application.yaml", + value, + optionalValue(is("from-file"))); + } +} diff --git a/config/tests/test-default_config-1-properties/pom.xml b/config/tests/test-default_config-1-properties/pom.xml index cc2ea411e84..1fde63d2a93 100644 --- a/config/tests/test-default_config-1-properties/pom.xml +++ b/config/tests/test-default_config-1-properties/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-default_config-1-properties Helidon Config Tests Default Config 1 diff --git a/config/tests/test-default_config-2-hocon-json/pom.xml b/config/tests/test-default_config-2-hocon-json/pom.xml index 87a6bb41fc9..0677d0ad338 100644 --- a/config/tests/test-default_config-2-hocon-json/pom.xml +++ b/config/tests/test-default_config-2-hocon-json/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-default_config-2-hocon-json Helidon Config Tests Default Config 2 diff --git a/config/tests/test-default_config-3-hocon/pom.xml b/config/tests/test-default_config-3-hocon/pom.xml index 2944128250c..43b04e9e738 100644 --- a/config/tests/test-default_config-3-hocon/pom.xml +++ b/config/tests/test-default_config-3-hocon/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-default_config-3-hocon Helidon Config Tests Default Config 3 diff --git a/config/tests/test-default_config-4-yaml/pom.xml b/config/tests/test-default_config-4-yaml/pom.xml index f84ddbe7310..b391fa3c6c8 100644 --- a/config/tests/test-default_config-4-yaml/pom.xml +++ b/config/tests/test-default_config-4-yaml/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-default_config-4-yaml Helidon Config Tests Default Config 4 diff --git a/config/tests/test-default_config-5-env_vars/pom.xml b/config/tests/test-default_config-5-env_vars/pom.xml index 85a739fc627..5b5b2554daa 100644 --- a/config/tests/test-default_config-5-env_vars/pom.xml +++ b/config/tests/test-default_config-5-env_vars/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-default_config-5-env_vars Helidon Config Tests Default Config 5 diff --git a/config/tests/test-default_config-6-meta-properties/pom.xml b/config/tests/test-default_config-6-meta-properties/pom.xml index e7aebf81c59..3358d58960a 100644 --- a/config/tests/test-default_config-6-meta-properties/pom.xml +++ b/config/tests/test-default_config-6-meta-properties/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-default_config-6-meta-properties Helidon Config Tests Default Config 6 diff --git a/config/tests/test-default_config-7-meta-hocon-json/pom.xml b/config/tests/test-default_config-7-meta-hocon-json/pom.xml index 9994c436966..fa05aa8bb5c 100644 --- a/config/tests/test-default_config-7-meta-hocon-json/pom.xml +++ b/config/tests/test-default_config-7-meta-hocon-json/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-default_config-7-meta-hocon-json Helidon Config Tests Default Config 7 diff --git a/config/tests/test-default_config-8-meta-hocon/pom.xml b/config/tests/test-default_config-8-meta-hocon/pom.xml index 3793915f2ef..d8098cadaff 100644 --- a/config/tests/test-default_config-8-meta-hocon/pom.xml +++ b/config/tests/test-default_config-8-meta-hocon/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-default_config-8-meta-hocon Helidon Config Tests Default Config 8 diff --git a/config/tests/test-default_config-9-meta-yaml/pom.xml b/config/tests/test-default_config-9-meta-yaml/pom.xml index 8753cfaca39..c99fe292c99 100644 --- a/config/tests/test-default_config-9-meta-yaml/pom.xml +++ b/config/tests/test-default_config-9-meta-yaml/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-default_config-9-meta-yaml Helidon Config Tests Default Config 9 diff --git a/config/tests/test-lazy-source/pom.xml b/config/tests/test-lazy-source/pom.xml index a139483f0ff..a95345fb710 100644 --- a/config/tests/test-lazy-source/pom.xml +++ b/config/tests/test-lazy-source/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-lazy-source Helidon Config Tests Lazy Source diff --git a/config/tests/test-mappers-1-common/pom.xml b/config/tests/test-mappers-1-common/pom.xml index 7585f642b1b..87e7056c51e 100644 --- a/config/tests/test-mappers-1-common/pom.xml +++ b/config/tests/test-mappers-1-common/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-mappers-1-common Helidon Config Tests Mappers Common 1 diff --git a/config/tests/test-mappers-2-complex/pom.xml b/config/tests/test-mappers-2-complex/pom.xml index d2032c6d19b..dab0f6e4b27 100644 --- a/config/tests/test-mappers-2-complex/pom.xml +++ b/config/tests/test-mappers-2-complex/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-mappers-2-complex Helidon Config Tests Parsers 2 diff --git a/config/tests/test-meta-source/pom.xml b/config/tests/test-meta-source/pom.xml index f91fc8c6bc4..6c7b574fdc2 100644 --- a/config/tests/test-meta-source/pom.xml +++ b/config/tests/test-meta-source/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-meta-source Helidon Config Tests Meta Source diff --git a/config/tests/test-mp-se-meta/pom.xml b/config/tests/test-mp-se-meta/pom.xml new file mode 100644 index 00000000000..693b23fa6a2 --- /dev/null +++ b/config/tests/test-mp-se-meta/pom.xml @@ -0,0 +1,67 @@ + + + + + 4.0.0 + + io.helidon.config.tests + helidon-config-tests-project + 4.2.0-SNAPSHOT + ../pom.xml + + helidon-config-tests-mp-se-meta + Helidon Config Tests MP SE Meta + + + Test that when using MP config with meta-config file name that conflicts with Helidon SE, everything + works as expected. + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-mp + + + io.helidon.config + helidon-config-yaml + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + diff --git a/config/tests/test-mp-se-meta/src/main/resources/app.properties b/config/tests/test-mp-se-meta/src/main/resources/app.properties new file mode 100644 index 00000000000..763fa6bd7c6 --- /dev/null +++ b/config/tests/test-mp-se-meta/src/main/resources/app.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +helidon.test.value=value diff --git a/config/tests/test-mp-se-meta/src/main/resources/application.yaml b/config/tests/test-mp-se-meta/src/main/resources/application.yaml new file mode 100644 index 00000000000..496e0c1ed88 --- /dev/null +++ b/config/tests/test-mp-se-meta/src/main/resources/application.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +helidon.app.value: "app-value" \ No newline at end of file diff --git a/config/tests/test-mp-se-meta/src/main/resources/meta-config.yaml b/config/tests/test-mp-se-meta/src/main/resources/meta-config.yaml new file mode 100644 index 00000000000..607b6f02e6d --- /dev/null +++ b/config/tests/test-mp-se-meta/src/main/resources/meta-config.yaml @@ -0,0 +1,22 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# This is an MP meta-config. It should not be named like this, but if the user does it, we should +# honor it +add-default-sources: true +sources: + - type: "properties" + classpath: "app.properties" \ No newline at end of file diff --git a/config/tests/test-mp-se-meta/src/test/java/io/helidon/config/tests/mpsemeta/MpSeMetaTest.java b/config/tests/test-mp-se-meta/src/test/java/io/helidon/config/tests/mpsemeta/MpSeMetaTest.java new file mode 100644 index 00000000000..46ef7958ee4 --- /dev/null +++ b/config/tests/test-mp-se-meta/src/test/java/io/helidon/config/tests/mpsemeta/MpSeMetaTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.tests.mpsemeta; + +import io.helidon.common.config.GlobalConfig; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MpSeMetaTest { + @Order(0) + @Test + public void testSeMeta() { + // this should not fail + io.helidon.config.Config config = io.helidon.config.Config.create(); + assertThat(config.get("helidon.app.value").asString().asOptional(), + optionalValue(is("app-value"))); + } + + @Order(1) + @Test + public void testMpMeta() { + System.setProperty("io.helidon.config.mp.meta-config", "meta-config.yaml"); + Config config = ConfigProvider.getConfig(); + assertThat(config.getValue("helidon.test.value", String.class), is("value")); + + assertThat(GlobalConfig.config() + .get("helidon.test.value") + .asString() + .asOptional(), optionalValue(is("value"))); + } +} diff --git a/config/tests/test-no-config-sources/pom.xml b/config/tests/test-no-config-sources/pom.xml new file mode 100644 index 00000000000..4566597c4a6 --- /dev/null +++ b/config/tests/test-no-config-sources/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + io.helidon.config.tests + helidon-config-tests-project + 4.2.0-SNAPSHOT + ../pom.xml + + helidon-config-tests-no-config-sources + Helidon Config Tests No Config Sources + + + Test that when no config sources are defined (such as when using Config.just()), we do not + fallback to meta configuration. + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + diff --git a/config/tests/test-no-config-sources/src/main/resources/application.yaml b/config/tests/test-no-config-sources/src/main/resources/application.yaml new file mode 100644 index 00000000000..cf3e2c3af54 --- /dev/null +++ b/config/tests/test-no-config-sources/src/main/resources/application.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +value: "from-file" diff --git a/config/tests/test-no-config-sources/src/main/resources/meta-config.yaml b/config/tests/test-no-config-sources/src/main/resources/meta-config.yaml new file mode 100644 index 00000000000..feac5d69543 --- /dev/null +++ b/config/tests/test-no-config-sources/src/main/resources/meta-config.yaml @@ -0,0 +1,20 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +sources: + - type: "inlined" + properties: + value: "from-meta-config" \ No newline at end of file diff --git a/config/tests/test-no-config-sources/src/test/java/io/helidon/config/tests/nosources/NoSourcesTest.java b/config/tests/test-no-config-sources/src/test/java/io/helidon/config/tests/nosources/NoSourcesTest.java new file mode 100644 index 00000000000..f0bf1ff212e --- /dev/null +++ b/config/tests/test-no-config-sources/src/test/java/io/helidon/config/tests/nosources/NoSourcesTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.config.tests.nosources; + +import java.util.List; +import java.util.Optional; + +import io.helidon.config.Config; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class NoSourcesTest { + @Test + public void testJust() { + Config config = Config.just(); + + Optional value = config.get("value") + .asString() + .asOptional(); + + assertThat("We have used Config.just(), there should be NO config source", value, is(optionalEmpty())); + } + + @Test + public void testBuilder() { + Config config = Config.builder() + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .sources(List.of()) + .build(); + + Optional value = config.get("value") + .asString() + .asOptional(); + + assertThat("We have used Config.builder() without specifying any source, there should be NO config source", + value, + is(optionalEmpty())); + } + + @Test + public void testMetaConfigExplicit() { + // a sanity check that meta configuration works when requested + + Config config = Config.builder() + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .metaConfig() + .build(); + + Optional value = config.get("value") + .asString() + .asOptional(); + + assertThat("We have used metaConfig(), there should be the inlined config source configured", + value, + optionalValue(is("from-meta-config"))); + } + + @Test + public void testMetaConfigCreate() { + // a sanity check that meta configuration works when requested + + Config config = Config.create(); + + Optional value = config.get("value") + .asString() + .asOptional(); + + assertThat("We have used Config.create(), there should be the inlined config source configured", + value, + optionalValue(is("from-meta-config"))); + } +} diff --git a/config/tests/test-parsers-1-complex/pom.xml b/config/tests/test-parsers-1-complex/pom.xml index 29e18fe2089..1565ad1a069 100644 --- a/config/tests/test-parsers-1-complex/pom.xml +++ b/config/tests/test-parsers-1-complex/pom.xml @@ -24,7 +24,7 @@ io.helidon.config.tests helidon-config-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-tests-test-parsers-1-complex Helidon Config Tests Parsers 1 diff --git a/config/yaml-mp/pom.xml b/config/yaml-mp/pom.xml index 04ef717ba06..6f14f27c1d5 100644 --- a/config/yaml-mp/pom.xml +++ b/config/yaml-mp/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-yaml-mp Helidon Config YAML MP diff --git a/config/yaml/pom.xml b/config/yaml/pom.xml index ffe145183a7..63f4a292bb2 100644 --- a/config/yaml/pom.xml +++ b/config/yaml/pom.xml @@ -24,7 +24,7 @@ io.helidon.config helidon-config-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-config-yaml Helidon Config YAML diff --git a/cors/pom.xml b/cors/pom.xml index c087d80befe..bc00ffcf430 100644 --- a/cors/pom.xml +++ b/cors/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.cors diff --git a/dbclient/dbclient/pom.xml b/dbclient/dbclient/pom.xml index 0a8b09fcc64..7b3caee48bc 100644 --- a/dbclient/dbclient/pom.xml +++ b/dbclient/dbclient/pom.xml @@ -22,7 +22,7 @@ io.helidon.dbclient helidon-dbclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-dbclient Helidon Database Client API diff --git a/dbclient/health/pom.xml b/dbclient/health/pom.xml index ea7eb87ecb1..8517ae24b2f 100644 --- a/dbclient/health/pom.xml +++ b/dbclient/health/pom.xml @@ -22,7 +22,7 @@ io.helidon.dbclient helidon-dbclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-dbclient-health Helidon Database Client Health diff --git a/dbclient/hikari/pom.xml b/dbclient/hikari/pom.xml index 6cffdc2e3d3..9205953f4ca 100644 --- a/dbclient/hikari/pom.xml +++ b/dbclient/hikari/pom.xml @@ -22,7 +22,7 @@ io.helidon.dbclient helidon-dbclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-dbclient-hikari Helidon Database Client Hikari Connection Pool diff --git a/dbclient/jdbc/pom.xml b/dbclient/jdbc/pom.xml index 79673075ce5..0ef9c3cb8f6 100644 --- a/dbclient/jdbc/pom.xml +++ b/dbclient/jdbc/pom.xml @@ -22,7 +22,7 @@ io.helidon.dbclient helidon-dbclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-dbclient-jdbc Helidon Database Client JDBC diff --git a/dbclient/jsonp/pom.xml b/dbclient/jsonp/pom.xml index 86dd4974c85..5483bde3827 100644 --- a/dbclient/jsonp/pom.xml +++ b/dbclient/jsonp/pom.xml @@ -21,7 +21,7 @@ io.helidon.dbclient helidon-dbclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 helidon-dbclient-jsonp diff --git a/dbclient/metrics-hikari/pom.xml b/dbclient/metrics-hikari/pom.xml index b544d69963f..8a4eb93fccb 100644 --- a/dbclient/metrics-hikari/pom.xml +++ b/dbclient/metrics-hikari/pom.xml @@ -22,7 +22,7 @@ io.helidon.dbclient helidon-dbclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-dbclient-metrics-hikari Helidon Database Client Hikari Pool Metrics diff --git a/dbclient/metrics/pom.xml b/dbclient/metrics/pom.xml index 976c9b1fa25..10083eea0b9 100644 --- a/dbclient/metrics/pom.xml +++ b/dbclient/metrics/pom.xml @@ -22,7 +22,7 @@ io.helidon.dbclient helidon-dbclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-dbclient-metrics Helidon Database Client Metrics diff --git a/dbclient/mongodb/pom.xml b/dbclient/mongodb/pom.xml index bf8a40dbf0e..d9a08cc15a4 100644 --- a/dbclient/mongodb/pom.xml +++ b/dbclient/mongodb/pom.xml @@ -22,7 +22,7 @@ io.helidon.dbclient helidon-dbclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-dbclient-mongodb Helidon Database Client MongoDB diff --git a/dbclient/pom.xml b/dbclient/pom.xml index f604c3e4510..2d6dedf451f 100644 --- a/dbclient/pom.xml +++ b/dbclient/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/dbclient/tracing/pom.xml b/dbclient/tracing/pom.xml index 3b696d94f3f..57486d9c27d 100644 --- a/dbclient/tracing/pom.xml +++ b/dbclient/tracing/pom.xml @@ -22,7 +22,7 @@ io.helidon.dbclient helidon-dbclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-dbclient-tracing Helidon Database Client Tracing diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 044ec587c30..a1a320f43fe 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-bom - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../bom/pom.xml helidon-dependencies @@ -53,7 +53,7 @@ 1.34.1 1.33.3 2.3.3 - 3.25.4 + 3.25.5 23.1.0 22.1 22.0 @@ -92,7 +92,7 @@ 1.3.0.Final 3.5.3.Final 4.0.3 - 3.1.7 + 3.1.8 6.7.0.202309050840-r 5.9.3 3.6.2 @@ -100,8 +100,8 @@ 1.4.14 2.6.2 2.10 - 1.11.3 - 1.11.3 + 1.13.4 + 1.13.4 3.8.7 3.4.3 4.8.0 @@ -127,13 +127,13 @@ 7.0.0.Final 5.12.0 4.1.108.Final - 3.46.1 + 3.50.0 21 - ${version.lib.ojdbc.family}.9.0.0 + ${version.lib.ojdbc.family}.15.0.0 ${version.lib.ojdbc} 3.4.0 @@ -604,6 +604,11 @@ micrometer-registry-prometheus ${version.lib.micrometer-prometheus} + + io.micrometer + micrometer-registry-prometheus-simpleclient + ${version.lib.micrometer-prometheus} + diff --git a/docs/pom.xml b/docs/pom.xml index 1c753de4850..2c20ea103ef 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-docs site @@ -86,6 +86,21 @@ postgresql true + + io.helidon.microprofile.lra + helidon-microprofile-lra-testing + true + + + org.hamcrest + hamcrest-all + true + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + true + diff --git a/docs/src/main/asciidoc/includes/attributes.adoc b/docs/src/main/asciidoc/includes/attributes.adoc index e1c80e22389..9cf1261c1b7 100644 --- a/docs/src/main/asciidoc/includes/attributes.adoc +++ b/docs/src/main/asciidoc/includes/attributes.adoc @@ -23,7 +23,7 @@ ifndef::attributes-included[] // functional attributes :imagesdir: {rootdir}/images -:helidon-version: 4.1.0-SNAPSHOT +:helidon-version: 4.2.0-SNAPSHOT :helidon-version-is-release: false :sourcedir: {rootdir}/../java/io/helidon/docs diff --git a/docs/src/main/asciidoc/includes/security/providers/abac.adoc b/docs/src/main/asciidoc/includes/security/providers/abac.adoc index 5a1f0820096..1909d7dc145 100644 --- a/docs/src/main/asciidoc/includes/security/providers/abac.adoc +++ b/docs/src/main/asciidoc/includes/security/providers/abac.adoc @@ -196,3 +196,15 @@ security: ---- include::{sourcedir}/includes/security/providers/AbacSnippets.java[tag=snippet_4, indent=0] ---- + +[source,yaml] +.Configuration example for `JAX-RS` over the configuration +---- +server: + features: + security: + endpoints: + - path: "/somePath" + config: + abac.policy-validator.statement: "${env.time.year >= 2017}" +---- \ No newline at end of file diff --git a/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc b/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc index 16d3b1e85cf..af3239ac18d 100644 --- a/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc +++ b/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc @@ -119,7 +119,9 @@ If a file named `mp-meta-config.yaml`, or `mp-meta-config.properties` is in the on the classpath, and there is no explicit setup of configuration in the code, the configuration will be loaded from the `meta-config` file. The location of the file can be overridden using system property `io.helidon.config.mp.meta-config`, -or environment variable `HELIDON_MP_META_CONFIG` +or environment variable `HELIDON_MP_META_CONFIG`. + +*Important Note:* Do not use custom files named `meta-config.*`, as even when using Micro-Profile, we still use Helidon configuration in some of our components, and this file would be recognized as a Helidon SE Meta Configuration file, which may cause erroneous behavior. [source,yaml] .Example of a YAML meta configuration file: diff --git a/docs/src/main/asciidoc/mp/guides/quickstart.adoc b/docs/src/main/asciidoc/mp/guides/quickstart.adoc index 51a93d0a212..8c13e2714b1 100644 --- a/docs/src/main/asciidoc/mp/guides/quickstart.adoc +++ b/docs/src/main/asciidoc/mp/guides/quickstart.adoc @@ -49,6 +49,11 @@ mvn -U archetype:generate -DinteractiveMode=false \ -Dpackage=io.helidon.examples.quickstart.mp ---- +TIP: If you are using an earlier version of Helidon and the above command +fails with `java.lang.NoSuchMethodError` then you can work-around the error +by replacing `archetype:generate` with `org.apache.maven.plugins:maven-archetype-plugin:3.2.1:generate`. +Or use the helidon CLI. + [source,bash] .Or alternatively run the helidon CLI ---- diff --git a/docs/src/main/asciidoc/mp/jaxrs/helidon-connector.adoc b/docs/src/main/asciidoc/mp/jaxrs/helidon-connector.adoc index cab34bcd99f..ab5b695cfcb 100644 --- a/docs/src/main/asciidoc/mp/jaxrs/helidon-connector.adoc +++ b/docs/src/main/asciidoc/mp/jaxrs/helidon-connector.adoc @@ -112,6 +112,11 @@ properties supported by the connector, their types, scopes and default values. |client, invocation |true +|jersey.config.client.request.expect.100.continue.processing +|`Boolean` +|client +|true + |jersey.connector.helidon.config |`io.helidon.config.Config` |client diff --git a/docs/src/main/asciidoc/mp/lra.adoc b/docs/src/main/asciidoc/mp/lra.adoc index 96a186976bd..1fcdc199d3d 100644 --- a/docs/src/main/asciidoc/mp/lra.adoc +++ b/docs/src/main/asciidoc/mp/lra.adoc @@ -24,6 +24,7 @@ :spec-version: 1.0-RC3 :spec-name: MicroProfile {feature-name} specification :javadoc-link: https://download.eclipse.org/microprofile/microprofile-lra-{spec-version}/apidocs/org/eclipse/microprofile/lra/annotation/ +:microtx-link: https://docs.oracle.com/en/database/oracle/transaction-manager-for-microservices/index.html :rootdir: {docdir}/.. include::{rootdir}/includes/mp.adoc[] @@ -36,8 +37,10 @@ include::{rootdir}/includes/mp.adoc[] * <> * <> * <> +* <> * <> ** <> +** <> ** <> ** <> * <> @@ -351,6 +354,73 @@ include::{sourcedir}/mp/LraSnippets.java[tag=snippet_13, indent=0] <5> Method which will be called by coordinator when LRA is completed <6> Method which will be called by coordinator when LRA is canceled +== Testing +Testing of JAX-RS resources with LRA can be challenging as LRA participant running in parallel with the test is needed. + +Helidon provides test coordinator which can be started automatically with additional socket on a random port within your +own Helidon application. You only need one extra test dependency to enable test coordinator in your xref:testing/testing.adoc[@HelidonTest]. + +[source, xml] +.Dependency +---- + + io.helidon.microprofile.lra + helidon-microprofile-lra-testing + test + +---- + +Considering that you have LRA enabled JAX-RS resource you want to test. + +[source, java] +.Example JAX-RS resource with LRA. +---- +include::{sourcedir}/mp/LraSnippets.java[tag=snippet_14, indent=0] +---- + +Helidon test with enabled CDI discovery can look like this. + +[source, java] +.HelidonTest with LRA test support. +---- +include::{sourcedir}/mp/LraSnippets.java[tag=snippet_15, indent=0] +---- +<1> Resource is discovered automatically +<2> Test coordinator needs to be added manually +<3> Injecting test coordinator to access state of LRA managed by coordinator mid-test +<4> Retrieving LRA managed by coordinator by LraId +<5> Asserting LRA state in coordinator + +LRA testing feature has the following default configuration: + +* port: `0` - coordinator is started on random port(Helidon LRA participant is capable to discover test coordinator automatically) +* bind-address: `localhost` - bind address of the coordinator +* helidon.lra.coordinator.persistence: `false` - LRAs managed by test coordinator are not persisted +* helidon.lra.participant.use-build-time-index: `false` - Participant annotation inspection ignores Jandex index files created in build time, it helps to avoid issues with additional test resources + +Testing LRA coordinator is started on additional named socket `test-lra-coordinator` configured with default index `500`. +Default index can be changed with system property `helidon.lra.coordinator.test-socket.index`. + +Example: `-Dhelidon.lra.coordinator.test-socket.index=20`. + +[source, java] +.HelidonTest override LRA test feature default settings. +---- +include::{sourcedir}/mp/LraSnippets.java[tag=snippet_16, indent=0] +---- +<1> Start test LRA coordinator always on the same port 8070(default is random port) +<2> Test LRA coordinator socket bind address (default is localhost) +<3> Persist LRA managed by coordinator(default is false) +<4> Use build time Jandex index(default is false) + +When CDI bean auto-discovery is not desired, LRA and Config CDI extensions needs to be added manually. + +[source, java] +.HelidonTest setup with disabled discovery. +---- +include::{sourcedir}/mp/LraSnippets.java[tag=snippet_17, indent=0] +---- + == Additional Information === Coordinator @@ -359,12 +429,65 @@ the participants when the LRA transaction gets cancelled or completes (in case i In addition, participant also keeps track of timeouts, retries participant calls, and assigns LRA ids. .Helidon LRA supports following coordinators +* {microtx-link}[MicroTx LRA coordinator] * Helidon LRA coordinator * https://narayana.io/lra[Narayana coordinator]. +=== MicroTx LRA Coordinator +Oracle Transaction Manager for Microservices - {microtx-link}[MicroTx] is an enterprise grade transaction manager for microservices, +among other it manages LRA transactions and is compatible with Narayana LRA clients. + +MicroTx LRA coordinator is compatible with Narayana clients when `narayanaLraCompatibilityMode` is on, +you need to add another dependency to enable Narayana client: +[source,xml] +.Dependency needed for using Helidon LRA with Narayana compatible coordinator +---- + + io.helidon.lra + helidon-lra-coordinator-narayana-client + +---- + +[source, bash] +.Run MicroTx in Docker +---- +docker container run --name otmm -v "$(pwd)":/app/config \ +-w /app/config -p 8080:8080/tcp --env CONFIG_FILE=tcs.yaml \ +--add-host host.docker.internal:host-gateway -d tmm: +---- + +To use MicroTx with Helidon LRA participant, `narayanaLraCompatibilityMode` needs to be enabled. + +[source, yaml] +.Configure MicroTx for development +---- +tmmAppName: tcs +tmmConfiguration: + listenAddr: 0.0.0.0:8080 + internalAddr: http://host.docker.internal:8080 + externalUrl: http://lra-coordinator.acme.com:8080 + xaCoordinator: + enabled: false + lraCoordinator: + enabled: true + tccCoordinator: + enabled: false + storage: + type: memory + authentication: + enabled: false + authorization: + enabled: false + serveTLS: + enabled: false + narayanaLraCompatibilityMode: + enabled: true #<1> +---- +<1> Enable Narayana compatibility mode + === Helidon LRA Coordinator -CAUTION: Experimental tool, usage in production is not advised. +CAUTION: Test tool, usage in production is not advised. [source,bash] .Build and run Helidon LRA coordinator @@ -417,3 +540,4 @@ with port `8070` defined in the snippet above you need to configure your Helidon * {microprofile-lra-spec-url}[{spec-name}] * https://download.eclipse.org/microprofile/microprofile-lra-{spec-version}/apidocs/org/eclipse/microprofile/lra/[Microprofile LRA JavaDoc] * https://helidon.io/docs/v4/apidocs/io.helidon.lra.coordinator.client/module-summary.html[Helidon LRA Client JavaDoc] +* {microtx-link}[MicroTx - Oracle Transaction Manager for Microservices] diff --git a/docs/src/main/asciidoc/mp/persistence.adoc b/docs/src/main/asciidoc/mp/persistence.adoc index 86e9df1c7fa..1459974ac73 100644 --- a/docs/src/main/asciidoc/mp/persistence.adoc +++ b/docs/src/main/asciidoc/mp/persistence.adoc @@ -327,7 +327,7 @@ the `test` data source's associated connection pool or vendor-specific [source,properties] ---- -javax.sql.DataSource.test.foo.bar=baz # <1> <2> <3> +javax.sql.DataSource.test.foo.bar=baz# <1><2><3> ---- <1> The *_objecttype_* portion of the configuration property name is `javax.sql.DataSource`. @@ -434,8 +434,8 @@ a service name of `XE`, a `user` of `scott`, and a `password` of [source,properties] ---- -javax.sql.DataSource.main.connectionFactoryClassName = oracle.jdbc.pool.OracleDataSource # <1> -javax.sql.DataSource.main.url = jdbc:oracle:thin://@localhost:1521/XE # <2> +javax.sql.DataSource.main.connectionFactoryClassName = oracle.jdbc.pool.OracleDataSource# <1> +javax.sql.DataSource.main.URL = jdbc:oracle:thin:@//localhost:1521/XE# <2> javax.sql.DataSource.main.user = scott javax.sql.DataSource.main.password = tiger ---- @@ -488,8 +488,8 @@ password: [source,properties] ---- -javax.sql.DataSource.test.dataSourceClassName = org.h2.jdbcx.JdbcDataSource # <1> -javax.sql.DataSource.test.dataSource.url = jdbc:h2:mem:unit-testing;DB_CLOSE_DELAY=-1 # <2> <3> +javax.sql.DataSource.test.dataSourceClassName = org.h2.jdbcx.JdbcDataSource# <1> +javax.sql.DataSource.test.dataSource.url = jdbc:h2:mem:unit-testing;DB_CLOSE_DELAY=-1# <2><3> javax.sql.DataSource.test.dataSource.user = sa javax.sql.DataSource.test.dataSource.password = ---- diff --git a/docs/src/main/asciidoc/mp/telemetry.adoc b/docs/src/main/asciidoc/mp/telemetry.adoc index 7fecd1c7349..7251e88c06e 100644 --- a/docs/src/main/asciidoc/mp/telemetry.adoc +++ b/docs/src/main/asciidoc/mp/telemetry.adoc @@ -48,6 +48,28 @@ include::{rootdir}/includes/dependencies.adoc[] ---- +[[otel-exporter-dependencies]] +Also, add a dependency on an OpenTelemetry exporter. +[source,xml] +.Example dependency for the OpenTelemetry OTLP exporter +---- + + io.opentelemetry + opentelemetry-exporter-otlp + +---- +[source,xml] +.Example dependency for the OpenTelemetry Jaeger exporter +---- + + io.opentelemetry + opentelemetry-exporter-jaeger + +---- + +Typical applications use a single exporter but you can add dependencies on multiple exporters and then use configuration to choose which to use in any given execution. +See the <> section for more details. + == Usage link:https://opentelemetry.io/[OpenTelemetry] comprises a collection of APIs, SDKs, integration tools, and other software components intended to facilitate the generation and control of telemetry data, including traces, metrics, and logs. In an environment where distributed tracing is enabled via OpenTelemetry (which combines OpenTracing and OpenCensus), this specification establishes the necessary behaviors for MicroProfile applications to participate seamlessly. @@ -61,7 +83,7 @@ MicroProfile Telemetry {mp-telemetry-version} allows for the exportation of the If possible, assign the following config setting in your application's `META-INF/microprofile-config.properties` file: [source,properties] ---- -mp.telemetry.span.name-includes-method = true +telemetry.span.name-includes-method = true ---- Earlier releases of Helidon 4 implemented MicroProfile Telemetry 1.0 which was based on OpenTelemetry semantic conventions 1.22.0-alpha. @@ -193,8 +215,10 @@ To configure OpenTelemetry, MicroProfile Config must be used, and the configurat Please consult with the links above for all configurations' properties usage. -The property should be declared in `microprofile-config.properties` file to be processed correctly. - +For your application to report trace information be sure you add a dependency on an OpenTelemetry exporter as <> and, as needed, configure its use. +By default OpenTelemetry attempts to use the OTLP exporter so you do not need to add configuration to specify that choice. +To use a different exporter set `otel.traces.exporter` in your configuration to the appropriate value: `jaeger`, `zipkin`, `prometheus`, etc. +See the <> section below. === OpenTelemetry Java Agent @@ -204,6 +228,7 @@ The OpenTelemetry Java Agent may influence the work of MicroProfile Telemetry, o This way, Helidon will explicitly get all the configuration and objects from the Agent, thus allowing correct span hierarchy settings. +[[examples]] == Examples This guide demonstrates how to incorporate MicroProfile Telemetry into Helidon and provides illustrations of how to view traces. Jaeger is employed in all the examples, and the Jaeger UI is used to view the traces. diff --git a/docs/src/main/asciidoc/se/guides/quickstart.adoc b/docs/src/main/asciidoc/se/guides/quickstart.adoc index 9bf0607ac2f..1a0ec936b8e 100644 --- a/docs/src/main/asciidoc/se/guides/quickstart.adoc +++ b/docs/src/main/asciidoc/se/guides/quickstart.adoc @@ -49,6 +49,11 @@ mvn -U archetype:generate -DinteractiveMode=false \ -Dpackage=io.helidon.examples.quickstart.se ---- +TIP: If you are using an earlier version of Helidon and the above command +fails with `java.lang.NoSuchMethodError` then you can work-around the error +by replacing `archetype:generate` with `org.apache.maven.plugins:maven-archetype-plugin:3.2.1:generate`. +Or use the helidon CLI. + [source,bash] .Or alternatively run the helidon CLI ---- diff --git a/docs/src/main/java/io/helidon/docs/mp/LraSnippets.java b/docs/src/main/java/io/helidon/docs/mp/LraSnippets.java index e859b9c4c55..e643d0d9aae 100644 --- a/docs/src/main/java/io/helidon/docs/mp/LraSnippets.java +++ b/docs/src/main/java/io/helidon/docs/mp/LraSnippets.java @@ -17,14 +17,33 @@ import java.net.URI; import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.logging.Logger; +import io.helidon.lra.coordinator.Lra; +import io.helidon.microprofile.config.ConfigCdiExtension; +import io.helidon.microprofile.lra.LraCdiExtension; +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.AddConfig; +import io.helidon.microprofile.testing.junit5.AddExtension; +import io.helidon.microprofile.testing.junit5.AddJaxRs; +import io.helidon.microprofile.testing.junit5.DisableDiscovery; +import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.microprofile.testing.lra.TestLraCoordinator; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.lra.LRAResponse; import org.eclipse.microprofile.lra.annotation.AfterLRA; @@ -36,10 +55,15 @@ import org.eclipse.microprofile.lra.annotation.Status; import org.eclipse.microprofile.lra.annotation.ws.rs.LRA; import org.eclipse.microprofile.lra.annotation.ws.rs.Leave; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER; import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_ENDED_CONTEXT_HEADER; import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_PARENT_CONTEXT_HEADER; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; @SuppressWarnings("ALL") class LraSnippets { @@ -218,4 +242,91 @@ public Response compensateExample(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraI // end::snippet_13[] } + // tag::snippet_14[] + @ApplicationScoped + @Path("/test") + public class WithdrawResource { + + private final List completedLras = new CopyOnWriteArrayList<>(); + private final List cancelledLras = new CopyOnWriteArrayList<>(); + + @PUT + @Path("/withdraw") + @LRA(LRA.Type.REQUIRES_NEW) + public Response withdraw(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) Optional lraId, String content) { + if ("BOOM".equals(content)) { + throw new IllegalArgumentException("BOOM"); + } + return Response.ok().build(); + } + + @Complete + public void complete(URI lraId) { + completedLras.add(lraId.toString()); + } + + @Compensate + public void rollback(URI lraId) { + cancelledLras.add(lraId.toString()); + } + + public List getCompletedLras() { + return completedLras; + } + } + // end::snippet_14[] + + // tag::snippet_15[] + @HelidonTest + //@AddBean(WithdrawResource.class) //<1> + @AddBean(TestLraCoordinator.class) //<2> + public class LraTest { + + @Inject + private WithdrawResource withdrawTestResource; + + @Inject + private TestLraCoordinator coordinator; //<3> + + @Inject + private WebTarget target; + + @Test + public void testLraComplete() { + try (Response res = target + .path("/test/withdraw") + .request() + .put(Entity.entity("test", MediaType.TEXT_PLAIN_TYPE))) { + assertThat(res.getStatus(), is(200)); + String lraId = res.getHeaderString(LRA.LRA_HTTP_CONTEXT_HEADER); + Lra lra = coordinator.lra(lraId); //<4> + assertThat(lra.status(), is(LRAStatus.Closed)); //<5> + assertThat(withdrawTestResource.getCompletedLras(), contains(lraId)); + } + } + } + // end::snippet_15[] + + // tag::snippet_16[] + @HelidonTest + @AddBean(TestLraCoordinator.class) + @AddConfig(key = "server.sockets.500.port", value = "8070") //<1> + @AddConfig(key = "server.sockets.500.bind-address", value = "custom.bind.name") //<2> + @AddConfig(key = "helidon.lra.coordinator.persistence", value = "true") //<3> + @AddConfig(key = "helidon.lra.participant.use-build-time-index", value = "true") //<4> + public class LraCustomConfigTest { + } + // end::snippet_16[] + + // tag::snippet_17[] + @HelidonTest + @DisableDiscovery + @AddJaxRs + @AddBean(TestLraCoordinator.class) + @AddExtension(LraCdiExtension.class) + @AddExtension(ConfigCdiExtension.class) + @AddBean(WithdrawResource.class) + public class LraNoDiscoveryTest { + } + // end::snippet_17[] } diff --git a/etc/dependency-check-suppression.xml b/etc/dependency-check-suppression.xml index 87c129b8a5e..2b0ff882133 100644 --- a/etc/dependency-check-suppression.xml +++ b/etc/dependency-check-suppression.xml @@ -2,6 +2,22 @@ + + + + + ^pkg:maven/org\.glassfish.*/(jakarta\.el|jaxb-core|jaxb-runtime|osgi-resource-locator|txw2)@.*$ + CVE-2024-9329 + + @@ -174,4 +190,22 @@ cpe:/a:mysql:mysql + + + + ^pkg:maven/io\.micrometer/micrometer-registry-prometheus-simpleclient@.*$ + CVE-2019-3826 + + + + ^pkg:maven/io\.prometheus/prometheus-metrics-(.*)@.*$ + CVE-2019-3826 + + diff --git a/etc/scripts/release.sh b/etc/scripts/release.sh index 8b0ac750564..bf7f0632574 100755 --- a/etc/scripts/release.sh +++ b/etc/scripts/release.sh @@ -23,7 +23,7 @@ on_error(){ CODE="${?}" && \ set +x && \ printf "[ERROR] Error(code=%s) occurred at %s:%s command: %s\n" \ - "${CODE}" "${BASH_SOURCE[0]}" "${LINENO}" "${BASH_COMMAND}" >&2 + "${CODE}" "${BASH_SOURCE[0]}" "${LINENO}" "${BASH_COMMAND}" >&2 } trap on_error ERR @@ -99,16 +99,16 @@ readonly COMMAND exec 6>&1 1>&2 if [ -z "${COMMAND+x}" ] ; then - echo "ERROR: no command provided" - exit 1 + echo "ERROR: no command provided" + exit 1 fi case ${COMMAND} in "update_version") if [ -z "${VERSION}" ] ; then - echo "ERROR: version required" >&2 - usage - exit 1 + echo "ERROR: version required" >&2 + usage + exit 1 fi ;; "create_tag"|"get_version") diff --git a/etc/scripts/smoketest.sh b/etc/scripts/smoketest.sh index a64cf6bba96..db49aba72b5 100755 --- a/etc/scripts/smoketest.sh +++ b/etc/scripts/smoketest.sh @@ -15,55 +15,26 @@ # limitations under the License. # -# Smoke test a Helidon release. This assumes: -# 1. The release has a source tag in the Helidon GitHub repo -# 2. The bits are in either the OSS Sonatype Staging Repo or Maven Central -# 3. You have a profile defined as "ossrh-staging" that configures -# https://oss.sonatype.org/content/groups/staging/ as a repository -# See bottom of RELEASE.md for details - set -o pipefail || true # trace ERR through pipes set -o errtrace || true # trace ERR through commands and functions set -o errexit || true # exit the script if any statement returns a non-true return value on_error(){ - CODE="${?}" && \ - set +x && \ - printf "[ERROR] Error(code=%s) occurred at %s:%s command: %s\n" \ - "${CODE}" "${BASH_SOURCE[0]}" "${LINENO}" "${BASH_COMMAND}" + CODE="${?}" && \ + set +x && \ + printf "[ERROR] Error(code=%s) occurred at %s:%s command: %s\n" \ + "${CODE}" "${BASH_SOURCE[0]}" "${LINENO}" "${BASH_COMMAND}" } trap on_error ERR -# Path to this script -if [ -h "${0}" ] ; then - SCRIPT_PATH="$(readlink "${0}")" -else - SCRIPT_PATH="${0}" -fi -readonly SCRIPT_PATH - - -SCRIPT_DIR=$(dirname "${SCRIPT_PATH}") -readonly SCRIPT_DIR - -# Local error handler -smoketest_on_error(){ - on_error - echo "===== Log file: ${OUTPUT_FILE} =====" - # In case there is a process left running -} - -# Setup error handling using local error handler (defined in includes/error_handlers.sh) -error_trap_setup 'smoketest_on_error' - usage(){ cat <&2 + usage exit 1 fi -set -u - -full(){ - echo "===== Full Test =====" - cd "${SCRATCH}" - quick - cd "${SCRATCH}" - - if [[ "${VERSION}" =~ .*SNAPSHOT ]]; then - echo "WARNING! SNAPSHOT version. Skipping tag checkout" - else - echo "===== Cloning Workspace ${GIT_URL} =====" - git clone "${GIT_URL}" - cd "${SCRATCH}/helidon" - echo "===== Checking out tags/${VERSION} =====" - git checkout "tags/${VERSION}" - fi - - echo "===== Building examples =====" - cd "${SCRATCH}/helidon/examples" - # XXX we exclude todo-app frontend due to the issues with npm behind firewall - mvn "${MAVEN_ARGS}" clean install -pl '!todo-app/frontend' ${STAGED_PROFILE} - cd "${SCRATCH}" - - echo "===== Building test support =====" - cd "${SCRATCH}/helidon/microprofile/tests/" - mvn -N "${MAVEN_ARGS}" clean install ${STAGED_PROFILE} - cd "${SCRATCH}/helidon/microprofile/tests/junit5" - mvn "${MAVEN_ARGS}" clean install ${STAGED_PROFILE} - cd "${SCRATCH}/helidon/microprofile/tests/junit5-tests" - mvn "${MAVEN_ARGS}" clean install ${STAGED_PROFILE} - - echo "===== Running tests =====" - cd "${SCRATCH}/helidon/tests" - mvn "${MAVEN_ARGS}" clean install ${STAGED_PROFILE} - - # Primes dependencies for native-image builds - cd "${SCRATCH}/helidon/tests/integration/native-image" - mvn "${MAVEN_ARGS}" clean install ${STAGED_PROFILE} - - echo "===== Running native image tests =====" - if [ -z "${GRAALVM_HOME}" ]; then - echo "WARNING! GRAALVM_HOME is not set. Skipping native image tests" - else - echo "GRAALVM_HOME=${GRAALVM_HOME}" - readonly native_image_tests="mp-1 mp-2 mp-3" - for native_test in ${native_image_tests}; do - cd "${SCRATCH}/helidon/tests/integration/native-image/${native_test}" - mvn "${MAVEN_ARGS}" clean package -Pnative-image ${STAGED_PROFILE} - done - - # Run this one because it has no pre-reqs and self-tests - cd "${SCRATCH}/helidon/tests/integration/native-image/mp-1" - target/helidon-tests-native-image-mp-1 - fi - +PID="" +trap '[ -n "${PID}" ] && kill ${PID} 2> /dev/null || true' 0 + +maven_proxies() { + [ -f "${HOME}/.m2/settings.xml" ] && \ + awk -f- "${HOME}/.m2/settings.xml" </{ + IN_PROXIES="true" + next + } + /<\/proxies>/{ + IN_PROXIES="false" + } + { + if (IN_PROXIES=="true") { + print \$0 + } + } +EOF } -waituntilready() { - # Give app a chance to start --retry will retry until it is up - # --retry-connrefused requires curl 7.51.0 or newer - sleep 6 - #curl -s --retry-connrefused --retry 3 -X GET http://localhost:8080/health/live - curl -s --retry 3 -X GET http://localhost:8080/health/live - echo +maven_settings() { + cat < + + $(maven_proxies) + + + + ossrh-staging + + + ossrh-staging + OSS Sonatype Staging + https://oss.sonatype.org/content/groups/staging/ + + false + + + true + + + + + + ossrh-staging + OSS Sonatype Staging + https://oss.sonatype.org/content/groups/staging/ + + false + + + true + + + + + + +EOF } -testGET() { - echo "GET $1" - http_code=$(curl -s -o /dev/null -w "%{http_code}" -X GET "${1}") - if [ "${http_code}" -ne "200" ]; then - echo "ERROR: Bad HTTP code. Expected 200 got ${http_code}. GET ${1}" - kill "${PID}" - return 1 - fi - return 0 +# arg1: uri +wait_ready() { + sleep 6 + if ! kill -0 "${PID}" 2> /dev/null ; then + echo "ERROR: process not alive" >&2 + return 1 + fi + + case ${1} in + bare-*) + # no-op + ;; + *-mp) + curl -q -s -f \ + --retry 3 \ + -o /dev/null \ + -w "%{http_code} %{url_effective}\n" \ + http://localhost:8080/health/live + ;; + *) + curl -q -s -f \ + --retry 3 \ + -o /dev/null \ + -w "%{http_code} %{url_effective}\n" \ + http://localhost:8080/observe/health/live + ;; + esac } -# -# $1 = archetype name: "quickstart-se" -buildAndTestArchetype(){ - archetype_name=${1} - archetype_pkg=$(echo "${archetype_name}" | tr "\-" "\.") - - echo "===== Testing Archetype ${archetype_name} =====" - - mvn "${MAVEN_ARGS}" -U archetype:generate -DinteractiveMode=false \ - -DarchetypeGroupId=io.helidon.archetypes \ - -DarchetypeArtifactId="helidon-${archetype_name}" \ - -DarchetypeVersion="${VERSION}" \ - -DgroupId=io.helidon.examples \ - -DartifactId=helidon-"${archetype_name}" \ - -Dpackage=io.helidon.examples."${archetype_pkg}" \ - ${STAGED_PROFILE} - - - echo "===== ${archetype_name}: building jar =====" - mvn "${MAVEN_ARGS}" -f helidon-"${archetype_name}"/pom.xml ${STAGED_PROFILE} clean package - - echo "===== Running and pinging ${archetype_name} app using jar =====" - java -jar "helidon-${archetype_name}/target/helidon-${archetype_name}.jar" & - PID=$! - testApp "${archetype_name}" - kill ${PID} - - echo "===== ${archetype_name}: building jlink image =====" - mvn "${MAVEN_ARGS}" -f "helidon-${archetype_name}/pom.xml" ${STAGED_PROFILE} -Pjlink-image package -DskipTests - - echo "===== Running and pinging ${archetype_name} app using jlink image =====" - "helidon-${archetype_name}/target/helidon-${archetype_name}-jri/bin/start" & - PID=$! - testApp "${archetype_name}" - kill ${PID} - sleep 1 +# arg1: url +http_get() { + curl -q -s -f \ + -w "\n%{http_code} %{url_effective}\n" \ + "${1}" } -testApp(){ - # Wait for app to come up - waituntilready - - # Hit some endpoints - if [ "${archetype_name}" = "quickstart-se" ] || [ "${archetype_name}" = "quickstart-mp" ]; then - testGET http://localhost:8080/greet - testGET http://localhost:8080/greet/Joe - fi - testGET http://localhost:8080/health - testGET http://localhost:8080/metrics +# arg1: archetype +test_app(){ + # health & metrics + case ${1} in + bare-*) + # no-op + ;; + *-se) + http_get http://localhost:8080/observe/health + http_get http://localhost:8080/observe/metrics + ;; + *-mp) + http_get http://localhost:8080/health + http_get http://localhost:8080/metrics + ;; + esac + + # app endpoint + case ${1} in + database-*) + # no-op + ;; + bare-se|quickstart-*) + http_get http://localhost:8080/greet + http_get http://localhost:8080/greet/Joe + ;; + bare-mp) + http_get http://localhost:8080/simple-greet + ;; + esac } -quick(){ - readonly archetypes=" - quickstart-se \ - quickstart-mp \ - bare-se \ - bare-mp \ - database-se \ - database-mp \ - " +# arg1: archetype +test_archetype(){ + printf "\n*******************************************" + printf "\nINFO: %s - Generating project" "${ARCHETYPE}" + printf "\n*******************************************\n\n" + + # shellcheck disable=SC2086 + mvn ${MAVEN_ARGS} -U \ + -DinteractiveMode=false \ + -DarchetypeGroupId=io.helidon.archetypes \ + -DarchetypeArtifactId="helidon-${ARCHETYPE}" \ + -DarchetypeVersion="${VERSION}" \ + -DgroupId=io.helidon.smoketest \ + -DartifactId=helidon-"${ARCHETYPE}" \ + -Dpackage=io.helidon.smoketest."${ARCHETYPE/-/.}" \ + archetype:generate + + printf "\n*******************************************" + printf "\nINFO: %s - Building jar" "${ARCHETYPE}" + printf "\n*******************************************\n\n" + + # shellcheck disable=SC2086 + mvn ${MAVEN_ARGS} \ + -f "helidon-${ARCHETYPE}/pom.xml" \ + clean package + + printf "\n*******************************************" + printf "\nINFO: %s - Running and pinging app using jar image" "${ARCHETYPE}" + printf "\n*******************************************\n\n" + + java -jar "helidon-${ARCHETYPE}/target/helidon-${ARCHETYPE}.jar" & + PID=${!} + wait_ready "${ARCHETYPE}" + test_app "${ARCHETYPE}" + kill ${PID} + + printf "\n*******************************************" + printf "\nINFO: %s - Building jlink image" "${ARCHETYPE}" + printf "\n*******************************************\n\n" + + # shellcheck disable=SC2086 + mvn ${MAVEN_ARGS} \ + -f "helidon-${ARCHETYPE}/pom.xml" \ + -DskipTests \ + -Pjlink-image \ + package + + printf "\n*******************************************" + printf "\nINFO: %s - Running and pinging app using jlink image" "${ARCHETYPE}" + printf "\n*******************************************\n\n" + + "helidon-${ARCHETYPE}/target/helidon-${ARCHETYPE}-jri/bin/start" & + PID=${!} + wait_ready "${ARCHETYPE}" + test_app "${ARCHETYPE}" + kill ${PID} +} - echo "===== Quick Test =====" - cd "${SCRATCH}" +WORK_DIR="${TMPDIR:-$(mktemp -d)}/helidon-smoke/${VERSION}-$(date +%Y-%m-%d-%H-%M-%S)" +readonly WORK_DIR - echo "===== Testing Archetypes =====" +LOG_FILE="${WORK_DIR}/test.log" +readonly LOG_FILE - for a in ${archetypes}; do - buildAndTestArchetype "${a}" - done -} +mkdir -p "${WORK_DIR}" -cd "${SCRATCH}" +maven_settings > "${WORK_DIR}/settings.xml" +MAVEN_ARGS="${MAVEN_ARGS} -s ${WORK_DIR}/settings.xml" -OUTPUT_FILE=${SCRATCH}/helidon-smoketest-log.txt -LOCAL_MVN_REPO=$(mvn "${MAVEN_ARGS}" help:evaluate -Dexpression=settings.localRepository | grep -v '\[INFO\]') -readonly OUTPUT_FILE LOCAL_MVN_REPO +exec 1>> >(tee "${LOG_FILE}") +exec 2>> >(tee "${LOG_FILE}") -echo "===== Running in ${SCRATCH} =====" -echo "===== Log file: ${OUTPUT_FILE} =====" +cd "${WORK_DIR}" -if [ -n "${CLEAN_MVN_REPO}" ] && [ -d "${LOCAL_MVN_REPO}" ]; then - echo "===== Cleaning release from local maven repository ${LOCAL_MVN_REPO} =====" - find "${LOCAL_MVN_REPO}/io/helidon" -depth -name "${VERSION}" -type d -exec rm -rf {} \; -fi +printf "\n*******************************************" +printf "\nINFO: Directory - %s" "${WORK_DIR}" +printf "\nINFO: Log - %s" "${LOG_FILE}" +printf "\n*******************************************\n\n" -# Invoke command -${COMMAND} | tee "${OUTPUT_FILE}" +test_archetype "${ARCHETYPE}" -echo "===== Log file: ${OUTPUT_FILE} =====" +printf "\n*******************************************" +printf "\nINFO: Directory - %s" "${WORK_DIR}" +printf "\nINFO: Log - %s" "${LOG_FILE}" +printf "\n*******************************************\n\n" diff --git a/fault-tolerance/fault-tolerance/pom.xml b/fault-tolerance/fault-tolerance/pom.xml index 39e8f42b20e..2a88f703817 100644 --- a/fault-tolerance/fault-tolerance/pom.xml +++ b/fault-tolerance/fault-tolerance/pom.xml @@ -22,7 +22,7 @@ io.helidon.fault-tolerance helidon-fault-tolerance-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-fault-tolerance diff --git a/fault-tolerance/pom.xml b/fault-tolerance/pom.xml index a3b5e4cbcf3..710b1866fd0 100644 --- a/fault-tolerance/pom.xml +++ b/fault-tolerance/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.fault-tolerance helidon-fault-tolerance-project diff --git a/graphql/pom.xml b/graphql/pom.xml index a39dec3a30d..8e793d44d0a 100644 --- a/graphql/pom.xml +++ b/graphql/pom.xml @@ -26,7 +26,7 @@ helidon-project io.helidon - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.graphql diff --git a/graphql/server/pom.xml b/graphql/server/pom.xml index d2eb6d9dda4..c757f204af7 100644 --- a/graphql/server/pom.xml +++ b/graphql/server/pom.xml @@ -25,7 +25,7 @@ io.helidon.graphql helidon-graphql-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-graphql-server diff --git a/grpc/api/pom.xml b/grpc/api/pom.xml index 25e8f98f9fc..825aa07dbe9 100644 --- a/grpc/api/pom.xml +++ b/grpc/api/pom.xml @@ -21,7 +21,7 @@ helidon-grpc-project io.helidon.grpc - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/grpc/core/pom.xml b/grpc/core/pom.xml index 072d008c7db..81b22cd6aae 100644 --- a/grpc/core/pom.xml +++ b/grpc/core/pom.xml @@ -21,7 +21,7 @@ helidon-grpc-project io.helidon.grpc - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/grpc/pom.xml b/grpc/pom.xml index 2729c49e310..074362c8969 100644 --- a/grpc/pom.xml +++ b/grpc/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/health/health-checks/pom.xml b/health/health-checks/pom.xml index 6d03d787962..d88af8adf13 100644 --- a/health/health-checks/pom.xml +++ b/health/health-checks/pom.xml @@ -22,7 +22,7 @@ io.helidon.health helidon-health-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-health-checks diff --git a/health/health/pom.xml b/health/health/pom.xml index 8dd1497bcd1..c5a2c9b6219 100644 --- a/health/health/pom.xml +++ b/health/health/pom.xml @@ -21,7 +21,7 @@ helidon-health-project io.helidon.health - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/health/pom.xml b/health/pom.xml index abcddd58648..fd06ce069f6 100644 --- a/health/pom.xml +++ b/health/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/helidon/pom.xml b/helidon/pom.xml index 3cd58d704e2..23401db4d21 100644 --- a/helidon/pom.xml +++ b/helidon/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon diff --git a/http/encoding/deflate/pom.xml b/http/encoding/deflate/pom.xml index baaea1372ef..d9b04d87361 100644 --- a/http/encoding/deflate/pom.xml +++ b/http/encoding/deflate/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.encoding helidon-http-encoding-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-encoding-deflate diff --git a/http/encoding/encoding/pom.xml b/http/encoding/encoding/pom.xml index 92208be8691..a9e6f8b68b0 100644 --- a/http/encoding/encoding/pom.xml +++ b/http/encoding/encoding/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.encoding helidon-http-encoding-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-encoding diff --git a/http/encoding/gzip/pom.xml b/http/encoding/gzip/pom.xml index f8c4a55c09a..a440048d243 100644 --- a/http/encoding/gzip/pom.xml +++ b/http/encoding/gzip/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.encoding helidon-http-encoding-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-encoding-gzip diff --git a/http/encoding/pom.xml b/http/encoding/pom.xml index e767147197f..a9adb7e7226 100644 --- a/http/encoding/pom.xml +++ b/http/encoding/pom.xml @@ -21,7 +21,7 @@ io.helidon.http helidon-http-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.http.encoding diff --git a/http/http/pom.xml b/http/http/pom.xml index 24157668137..1e332fe2808 100644 --- a/http/http/pom.xml +++ b/http/http/pom.xml @@ -23,7 +23,7 @@ io.helidon.http helidon-http-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http diff --git a/http/http/src/main/java/io/helidon/http/HostValidator.java b/http/http/src/main/java/io/helidon/http/HostValidator.java new file mode 100644 index 00000000000..834a27b0b5a --- /dev/null +++ b/http/http/src/main/java/io/helidon/http/HostValidator.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.http; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Validate the host string (maybe from the {@code Host} header). + *

+ * Validation is based on + * RFC-3986. + */ +public final class HostValidator { + private static final Pattern IP_V4_PATTERN = + Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$"); + private static final boolean[] HEXDIGIT = new boolean[256]; + private static final boolean[] UNRESERVED = new boolean[256]; + private static final boolean[] SUB_DELIMS = new boolean[256]; + + static { + // digits + for (int i = '0'; i <= '9'; i++) { + UNRESERVED[i] = true; + } + // alpha + for (int i = 'a'; i <= 'z'; i++) { + UNRESERVED[i] = true; + } + for (int i = 'A'; i <= 'Z'; i++) { + UNRESERVED[i] = true; + } + UNRESERVED['-'] = true; + UNRESERVED['.'] = true; + UNRESERVED['_'] = true; + UNRESERVED['~'] = true; + + // hexdigits + // digits + for (int i = '0'; i <= '9'; i++) { + HEXDIGIT[i] = true; + } + // alpha + for (int i = 'a'; i <= 'f'; i++) { + HEXDIGIT[i] = true; + } + for (int i = 'A'; i <= 'F'; i++) { + HEXDIGIT[i] = true; + } + + // sub-delim set + SUB_DELIMS['!'] = true; + SUB_DELIMS['$'] = true; + SUB_DELIMS['&'] = true; + SUB_DELIMS['\''] = true; + SUB_DELIMS['('] = true; + SUB_DELIMS[')'] = true; + SUB_DELIMS['*'] = true; + SUB_DELIMS['+'] = true; + SUB_DELIMS[','] = true; + SUB_DELIMS[';'] = true; + SUB_DELIMS['='] = true; + } + + private HostValidator() { + } + + /** + * Validate a host string. + * + * @param host host to validate + * @throws java.lang.IllegalArgumentException in case the host is not valid, the message is HTML encoded + */ + public static void validate(String host) { + Objects.requireNonNull(host); + if (host.indexOf('[') == 0 && host.indexOf(']') == host.length() - 1) { + validateIpLiteral(host); + } else { + validateNonIpLiteral(host); + } + } + + /** + * An IP literal starts with {@code [} and ends with {@code ]}. + * + * @param ipLiteral host literal string, may be an IPv6 address, or IP version future + * @throws java.lang.IllegalArgumentException in case the host is not valid, the message is HTML encoded + */ + public static void validateIpLiteral(String ipLiteral) { + Objects.requireNonNull(ipLiteral); + checkNotBlank("IP Literal", ipLiteral, ipLiteral); + + // IP-literal = "[" ( IPv6address / IPvFuture ) "]" + if (ipLiteral.charAt(0) != '[' || ipLiteral.charAt(ipLiteral.length() - 1) != ']') { + throw new IllegalArgumentException("Invalid IP literal, missing square bracket(s): " + HtmlEncoder.encode(ipLiteral)); + } + + String host = ipLiteral.substring(1, ipLiteral.length() - 1); + checkNotBlank("Host", ipLiteral, host); + if (host.charAt(0) == 'v') { + // IP future - starts with version `v1` etc. + validateIpFuture(ipLiteral, host); + return; + } + // IPv6 + /* + IPv6address = 6( h16 ":" ) ls32 + / "::" 5( h16 ":" ) ls32 + / [ h16 ] "::" 4( h16 ":" ) ls32 + / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + / [ *4( h16 ":" ) h16 ] "::" ls32 + / [ *5( h16 ":" ) h16 ] "::" h16 + / [ *6( h16 ":" ) h16 ] "::" + + ls32 = ( h16 ":" h16 ) / IPv4address + h16 = 1*4HEXDIG + */ + if (host.equals("::")) { + // all empty + return; + } + if (host.equals("::1")) { + // localhost + return; + } + boolean skipped = false; + int segments = 0; // max segments is 8 (full IPv6 address) + String inProgress = host; + while (!inProgress.isEmpty()) { + if (inProgress.length() == 1) { + segments++; + validateH16(ipLiteral, inProgress); + break; + } + if (inProgress.charAt(0) == ':' && inProgress.charAt(1) == ':') { + // :: means skip everything that was before (or everything that is after) + if (skipped) { + throw new IllegalArgumentException("Host IPv6 contains more than one skipped segment: " + + HtmlEncoder.encode(ipLiteral)); + } + skipped = true; + segments++; + inProgress = inProgress.substring(2); + continue; + } + if (inProgress.charAt(0) == ':') { + throw new IllegalArgumentException("Host IPv6 contains excessive colon: " + HtmlEncoder.encode(ipLiteral)); + } + // this must be h16 (or an IPv4 address) + int nextColon = inProgress.indexOf(':'); + if (nextColon == -1) { + // the rest of the string + if (inProgress.indexOf('.') == -1) { + segments++; + validateH16(ipLiteral, inProgress); + } else { + Matcher matcher = IP_V4_PATTERN.matcher(inProgress); + if (matcher.matches()) { + validateIpOctet("Host IPv6 dual address contains invalid IPv4 address:", ipLiteral, matcher.group(1)); + validateIpOctet("Host IPv6 dual address contains invalid IPv4 address:", ipLiteral, matcher.group(2)); + validateIpOctet("Host IPv6 dual address contains invalid IPv4 address:", ipLiteral, matcher.group(3)); + validateIpOctet("Host IPv6 dual address contains invalid IPv4 address:", ipLiteral, matcher.group(4)); + } else { + throw new IllegalArgumentException("Host IPv6 dual address contains invalid IPv4 address: " + + HtmlEncoder.encode(ipLiteral)); + } + } + break; + } + validateH16(ipLiteral, inProgress.substring(0, nextColon)); + segments++; + if (inProgress.length() >= nextColon + 2) { + if (inProgress.charAt(nextColon + 1) == ':') { + // double colon, keep it there + inProgress = inProgress.substring(nextColon); + continue; + } + } + inProgress = inProgress.substring(nextColon + 1); + if (inProgress.isBlank()) { + // this must fail on empty segment + validateH16(ipLiteral, inProgress); + } + } + + if (segments > 8) { + throw new IllegalArgumentException("Host IPv6 address contains too many segments: " + HtmlEncoder.encode(ipLiteral)); + } + } + + /** + * Validate IPv4 address or a registered name. + * + * @param host string with either an IPv4 address, or a registered name + * @throws java.lang.IllegalArgumentException in case the host is not valid, the message is HTML encoded + */ + public static void validateNonIpLiteral(String host) { + Objects.requireNonNull(host); + checkNotBlank("Host", host, host); + + // Ipv4 address: 127.0.0.1 + Matcher matcher = IP_V4_PATTERN.matcher(host); + if (matcher.matches()) { + /* + IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet + dec-octet = DIGIT ; 0-9 + / %x31-39 DIGIT ; 10-99 + / "1" 2DIGIT ; 100-199 + / "2" %x30-34 DIGIT ; 200-249 + / "25" %x30-35 ; 250-255 + */ + + // we have found an IPv4 address, or a valid registered name (555.555.555.555 is a valid name...) + return; + } + + // everything else is a registered name + + // registered name + /* + reg-name = *( unreserved / pct-encoded / sub-delims ) + pct-encoded = "%" HEXDIG HEXDIG + unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + / "*" / "+" / "," / ";" / "=" + */ + char[] charArray = host.toCharArray(); + for (int i = 0; i < charArray.length; i++) { + char c = charArray[i]; + if (c > 255) { + throw new IllegalArgumentException("Host contains invalid character: " + HtmlEncoder.encode(host)); + } + if (UNRESERVED[c]) { + continue; + } + if (SUB_DELIMS[c]) { + continue; + } + if (c == '%') { + // percent encoding + if (i + 2 >= charArray.length) { + throw new IllegalArgumentException("Host contains invalid % encoding: " + HtmlEncoder.encode(host)); + } + char p1 = charArray[++i]; + char p2 = charArray[++i]; + // %p1p2 + if (p1 > 255 || p2 > 255) { + throw new IllegalArgumentException("Host contains invalid character in % encoding: " + + HtmlEncoder.encode(host)); + } + if (HEXDIGIT[p1] && HEXDIGIT[p2]) { + continue; + } + throw new IllegalArgumentException("Host contains non-hexadecimal character in % encoding: " + + HtmlEncoder.encode(host)); + } + throw new IllegalArgumentException("Host contains invalid character: " + HtmlEncoder.encode(host)); + } + } + + private static void validateH16(String host, String inProgress) { + if (inProgress.isBlank()) { + throw new IllegalArgumentException("IPv6 segment is empty: " + HtmlEncoder.encode(host)); + } + if (inProgress.length() > 4) { + throw new IllegalArgumentException("IPv6 segment has more than 4 characters: " + HtmlEncoder.encode(host)); + } + validateHexDigits("IPv6 segment", host, inProgress); + } + + private static void validateHexDigits(String description, String host, String segment) { + for (char c : segment.toCharArray()) { + if (c > 255) { + throw new IllegalArgumentException(description + " non hexadecimal character: " + HtmlEncoder.encode(host)); + } + if (!HEXDIGIT[c]) { + throw new IllegalArgumentException(description + " non hexadecimal character: " + HtmlEncoder.encode(host)); + } + } + } + + private static void validateIpOctet(String message, String host, String octet) { + int octetInt = Integer.parseInt(octet); + // cannot be negative, as the regexp will not match + if (octetInt > 255) { + throw new IllegalArgumentException(message + " " + HtmlEncoder.encode(host)); + } + } + + private static void validateIpFuture(String ipLiteral, String host) { + /* + IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) + unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + */ + int dot = host.indexOf('.'); + if (dot == -1) { + throw new IllegalArgumentException("IP Future must contain 'v.': " + HtmlEncoder.encode(ipLiteral)); + } + // always starts with v + String version = host.substring(1, dot); + checkNotBlank("Version", ipLiteral, version); + validateHexDigits("Future version", ipLiteral, version); + + String address = host.substring(dot + 1); + checkNotBlank("IP Future", ipLiteral, address); + + for (char c : address.toCharArray()) { + if (c > 255) { + throw new IllegalArgumentException("Host contains invalid character: " + HtmlEncoder.encode(ipLiteral)); + } + if (UNRESERVED[c]) { + continue; + } + if (SUB_DELIMS[c]) { + continue; + } + if (c == ':') { + continue; + } + throw new IllegalArgumentException("Host contains invalid character: " + HtmlEncoder.encode(ipLiteral)); + } + } + + private static void checkNotBlank(String message, String ipLiteral, String toValidate) { + if (toValidate.isBlank()) { + throw new IllegalArgumentException(message + " cannot be blank: " + HtmlEncoder.encode(ipLiteral)); + } + } +} diff --git a/http/http/src/test/java/io/helidon/http/HostValidatorTest.java b/http/http/src/test/java/io/helidon/http/HostValidatorTest.java new file mode 100644 index 00000000000..f0144f9e787 --- /dev/null +++ b/http/http/src/test/java/io/helidon/http/HostValidatorTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.http; + +import org.junit.jupiter.api.Test; + +import static io.helidon.http.HostValidator.validate; +import static io.helidon.http.HostValidator.validateIpLiteral; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class HostValidatorTest { + @Test + void testGoodHostname() { + // sanity + validate("localhost"); + // host names + validate("www.example.com"); + // percent encoded + validate("%65%78%61%6D%70%6C%65"); + validate("%65%78%61%6D%70%6C%65.com"); + // with underscores + validate("www.exa_mple.com"); + // with sub-delims + validate("www.exa$mple.com"); + } + + @Test + void testGoodIp4() { + // IPv4 + validate("192.167.1.1"); + } + + @Test + void testGoodIpLiteral6() { + // IPv6 + validate("[2001:0db8:0001:0000:0000:0ab9:C0A8:0102]"); + validate("[::1]"); + validate("[2001:db8:3333:4444:5555:6666:7777:8888]"); + validate("[2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF]"); + validate("[::]"); + validate("[2001:db8::]"); + validate("[::1234:5678]"); + validate("[::1234:5678:1]"); + validate("[2001:db8::1234:5678]"); + validate("[2001:db8:1::ab9:C0A8:102]"); + } + + @Test + void testGoodIpLiteral6Dual() { + // IPv6 + validate("[2001:db8:3333:4444:5555:6666:1.2.3.4]"); + validate("[::11.22.33.44]"); + validate("[2001:db8::123.123.123.123]"); + validate("[::1234:5678:91.123.4.56]"); + validate("[::1234:5678:1.2.3.4]"); + validate("[2001:db8::1234:5678:5.6.7.8]"); + } + + @Test + void testGoodIpLiteralFuture() { + // IPvFuture + validate("[v9.abc:def]"); + validate("[v9.abc:def*]"); + } + + @Test + void testBadHosts() { + // just empty + invokeExpectFailure("Host cannot be blank: ", ""); + // invalid brackets + invokeExpectFailure("Host contains invalid character: [start.but.not.end", + "[start.but.not.end"); + invokeExpectFailure("Host contains invalid character: end.but.not.start]", + "end.but.not.start]"); + invokeExpectFailure("Host contains invalid character: int.the[.middle]", + "int.the[.middle]"); + // invalid escape + invokeExpectFailure("Host contains non-hexadecimal character in % encoding: www.%ZAxample.com", + "www.%ZAxample.com"); + invokeExpectFailure("Host contains non-hexadecimal character in % encoding: www.%AZxample.com", + "www.%AZxample.com"); + // invalid character (non-ASCII + invokeExpectFailure("Host contains invalid character: www.čexample.com", + "www.čexample.com"); + // wrong trailing escape (must be two chars); + invokeExpectFailure("Host contains invalid % encoding: www.example.com%4", + "www.example.com%4"); + invokeExpectFailure("Host contains invalid character in % encoding: www.example.com%č4", + "www.example.com%č4"); + invokeExpectFailure("Host contains invalid character in % encoding: www.example.com%4č", + "www.example.com%4č"); + } + + @Test + void testBadLiteral6() { + // IPv6 + // empty segment + invokeExpectFailure("Host IPv6 contains more than one skipped segment: [2001:db8::85a3::7334]", + "[2001:db8::85a3::7334]"); + // wrong segment (G is not a hexadecimal number) + invokeExpectFailure("IPv6 segment non hexadecimal character: " + + "[GGGG:FFFF:0000:0000:0000:0000:0000:0000]", + "[GGGG:FFFF:0000:0000:0000:0000:0000:0000]"); + // non-ASCII character + invokeExpectFailure("IPv6 segment non hexadecimal character: " + + "[č:FFFF:0000:0000:0000:0000:0000:0000]", + "[č:FFFF:0000:0000:0000:0000:0000:0000]"); + // wrong segment (too many characters) + invokeExpectFailure("IPv6 segment has more than 4 characters: [aaaaa:FFFF:0000:0000:0000:0000:0000:0000]", + "[aaaaa:FFFF:0000:0000:0000:0000:0000:0000]"); + // empty segment + invokeExpectFailure("IPv6 segment is empty: [aaaa:FFFF:0000:0000:0000:0000:0000:]", + "[aaaa:FFFF:0000:0000:0000:0000:0000:]"); + // wrong number of segments + invokeExpectFailure("Host IPv6 address contains too many segments: " + + "[0000:0000:0000:0000:0000:0000:0000:0000:0000:0000]", + "[0000:0000:0000:0000:0000:0000:0000:0000:0000:0000]"); + // missing everything + invokeExpectFailure("Host cannot be blank: []", + "[]"); + // wrong start (leading colon) + invokeExpectFailure("Host IPv6 contains excessive colon: [:1:0::]", + "[:1:0::]"); + // wrong end, colon instead of value + invokeExpectFailure("IPv6 segment non hexadecimal character: [1:0:::]", + "[1:0:::]"); + + invokeLiteralExpectFailure("Invalid IP literal, missing square bracket(s): [::", + "[::"); + invokeLiteralExpectFailure("Invalid IP literal, missing square bracket(s): ::]", + "::]"); + } + + @Test + void testBadLiteralDual() { + invokeLiteralExpectFailure("Host IPv6 dual address contains invalid IPv4 address: [::14.266.44.74]", + "[::14.266.44.74]"); + invokeLiteralExpectFailure("Host IPv6 dual address contains invalid IPv4 address: [::14.266.44]", + "[::14.266.44]"); + invokeLiteralExpectFailure("Host IPv6 dual address contains invalid IPv4 address: [::14.123.-44.147]", + "[::14.123.-44.147]"); + } + + @Test + void testBadLiteralFuture() { + // IPv future + // version must be present + invokeExpectFailure("Version cannot be blank: [v.abc:def]", + "[v.abc:def]"); + // missing address + invokeExpectFailure("IP Future must contain 'v.': [v2]", + "[v2]"); + invokeExpectFailure("IP Future cannot be blank: [v2.]", + "[v2.]"); + // invalid character in the host (valid future) + invokeExpectFailure("Host contains invalid character: [v2./0:::]", + "[v2./0:::]"); + invokeExpectFailure("Host contains invalid character: [v2.0:č]", + "[v2.0:č]"); + } + + private static void invokeExpectFailure(String message, String host) { + var t = assertThrows(IllegalArgumentException.class, () -> validate(host), "Testing host: " + host); + assertThat(t.getMessage(), is(message)); + } + + private static void invokeLiteralExpectFailure(String message, String host) { + var t = assertThrows(IllegalArgumentException.class, () -> validateIpLiteral(host), "Testing host: " + host); + assertThat(t.getMessage(), is(message)); + } +} \ No newline at end of file diff --git a/http/http2/pom.xml b/http/http2/pom.xml index f2e19a94535..11f6fa006ea 100644 --- a/http/http2/pom.xml +++ b/http/http2/pom.xml @@ -21,7 +21,7 @@ io.helidon.http helidon-http-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-http2 diff --git a/http/media/jackson/pom.xml b/http/media/jackson/pom.xml index 23580209ffd..5e445986f89 100644 --- a/http/media/jackson/pom.xml +++ b/http/media/jackson/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.media helidon-http-media-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-media-jackson diff --git a/http/media/jsonb/pom.xml b/http/media/jsonb/pom.xml index 1000e528905..34f9ee31064 100644 --- a/http/media/jsonb/pom.xml +++ b/http/media/jsonb/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.media helidon-http-media-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-media-jsonb diff --git a/http/media/jsonp/pom.xml b/http/media/jsonp/pom.xml index 839d4c7d3e4..4a5d49ecdd6 100644 --- a/http/media/jsonp/pom.xml +++ b/http/media/jsonp/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.media helidon-http-media-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-media-jsonp diff --git a/http/media/media/pom.xml b/http/media/media/pom.xml index 337f321f8c3..25b5c09be60 100644 --- a/http/media/media/pom.xml +++ b/http/media/media/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.media helidon-http-media-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-media diff --git a/http/media/multipart/pom.xml b/http/media/multipart/pom.xml index 026fdf4e8e1..0988ec65a70 100644 --- a/http/media/multipart/pom.xml +++ b/http/media/multipart/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.media helidon-http-media-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-media-multipart diff --git a/http/media/pom.xml b/http/media/pom.xml index 1d3d66d8ebc..498ef009867 100644 --- a/http/media/pom.xml +++ b/http/media/pom.xml @@ -21,7 +21,7 @@ io.helidon.http helidon-http-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.http.media diff --git a/http/pom.xml b/http/pom.xml index 2b878ecf8a0..111a4d0655d 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -24,7 +24,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.http diff --git a/http/sse/pom.xml b/http/sse/pom.xml index 5c2e0e78493..cc093208e32 100644 --- a/http/sse/pom.xml +++ b/http/sse/pom.xml @@ -21,7 +21,7 @@ io.helidon.http helidon-http-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-sse diff --git a/http/tests/encoding/deflate/pom.xml b/http/tests/encoding/deflate/pom.xml index 9485a7f5c7b..f177f928fd8 100644 --- a/http/tests/encoding/deflate/pom.xml +++ b/http/tests/encoding/deflate/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.tests.encoding helidon-http-tests-encoding-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-tests-integration-encoding-deflate diff --git a/http/tests/encoding/gzip/pom.xml b/http/tests/encoding/gzip/pom.xml index 2c5c0d387fc..94e05701629 100644 --- a/http/tests/encoding/gzip/pom.xml +++ b/http/tests/encoding/gzip/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.tests.encoding helidon-http-tests-encoding-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-tests-encoding-gzip diff --git a/http/tests/encoding/pom.xml b/http/tests/encoding/pom.xml index ed492830d8d..bd103ffb456 100644 --- a/http/tests/encoding/pom.xml +++ b/http/tests/encoding/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.tests helidon-http-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.http.tests.encoding diff --git a/http/tests/media/jsonb/pom.xml b/http/tests/media/jsonb/pom.xml index 4d041afbb5d..9deece9d136 100644 --- a/http/tests/media/jsonb/pom.xml +++ b/http/tests/media/jsonb/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.tests.media helidon-http-tests-media-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-tests-tegration-media-jsonb diff --git a/http/tests/media/jsonp/pom.xml b/http/tests/media/jsonp/pom.xml index 5c60f3bd9a6..a1e91f67f44 100644 --- a/http/tests/media/jsonp/pom.xml +++ b/http/tests/media/jsonp/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.tests.media helidon-http-tests-media-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-tests-media-jsonp diff --git a/http/tests/media/multipart/pom.xml b/http/tests/media/multipart/pom.xml index aa96e980ded..c5b784d9085 100644 --- a/http/tests/media/multipart/pom.xml +++ b/http/tests/media/multipart/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.tests.media helidon-http-tests-media-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-tests-media-multipart diff --git a/http/tests/media/pom.xml b/http/tests/media/pom.xml index 5514e1dc659..9121efcfd3f 100644 --- a/http/tests/media/pom.xml +++ b/http/tests/media/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.tests helidon-http-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.http.tests.media diff --git a/http/tests/media/string/pom.xml b/http/tests/media/string/pom.xml index cbf67e3629f..d6796b3eec9 100644 --- a/http/tests/media/string/pom.xml +++ b/http/tests/media/string/pom.xml @@ -21,7 +21,7 @@ io.helidon.http.tests.media helidon-http-tests-media-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-http-tests-media-string diff --git a/http/tests/pom.xml b/http/tests/pom.xml index 2523e745903..e2cf326667c 100644 --- a/http/tests/pom.xml +++ b/http/tests/pom.xml @@ -24,7 +24,7 @@ io.helidon.http helidon-http-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.http.tests diff --git a/inject/api/pom.xml b/inject/api/pom.xml index e17709048a2..b2a74861e48 100644 --- a/inject/api/pom.xml +++ b/inject/api/pom.xml @@ -21,7 +21,7 @@ io.helidon.inject helidon-inject-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/configdriven/api/pom.xml b/inject/configdriven/api/pom.xml index b338be01a4a..b82be424553 100644 --- a/inject/configdriven/api/pom.xml +++ b/inject/configdriven/api/pom.xml @@ -24,7 +24,7 @@ io.helidon.inject.configdriven helidon-inject-configdriven-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-inject-configdriven-api Helidon Injection ConfigDriven ConfiguredBy diff --git a/inject/configdriven/pom.xml b/inject/configdriven/pom.xml index 1bdd547b5a7..ced2fec42a2 100644 --- a/inject/configdriven/pom.xml +++ b/inject/configdriven/pom.xml @@ -24,7 +24,7 @@ io.helidon.inject helidon-inject-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.inject.configdriven helidon-inject-configdriven-project diff --git a/inject/configdriven/processor/pom.xml b/inject/configdriven/processor/pom.xml index dbb96fe3294..61c29c43d0d 100644 --- a/inject/configdriven/processor/pom.xml +++ b/inject/configdriven/processor/pom.xml @@ -24,7 +24,7 @@ io.helidon.inject.configdriven helidon-inject-configdriven-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-inject-configdriven-processor Helidon Injection ConfigDriven Processor diff --git a/inject/configdriven/runtime/pom.xml b/inject/configdriven/runtime/pom.xml index fb1e0a69867..6c7d9b262f9 100644 --- a/inject/configdriven/runtime/pom.xml +++ b/inject/configdriven/runtime/pom.xml @@ -24,7 +24,7 @@ io.helidon.inject.configdriven helidon-inject-configdriven-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-inject-configdriven-runtime Helidon Injection ConfigDriven Runtime Services diff --git a/inject/configdriven/tests/config/pom.xml b/inject/configdriven/tests/config/pom.xml index d2df352362d..6b9e9f6b867 100644 --- a/inject/configdriven/tests/config/pom.xml +++ b/inject/configdriven/tests/config/pom.xml @@ -22,7 +22,7 @@ io.helidon.inject.configdriven.tests helidon-inject-configdriven-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-inject-configdriven-tests-config Helidon Injection ConfigDriven Tests Config diff --git a/inject/configdriven/tests/configuredby-application/pom.xml b/inject/configdriven/tests/configuredby-application/pom.xml index b4f667737ba..7b892c2f5f6 100644 --- a/inject/configdriven/tests/configuredby-application/pom.xml +++ b/inject/configdriven/tests/configuredby-application/pom.xml @@ -22,7 +22,7 @@ io.helidon.inject.configdriven.tests helidon-inject-configdriven-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-inject-configdriven-tests-configuredby-application Helidon Injection ConfigDriven Tests CfgedBy App diff --git a/inject/configdriven/tests/configuredby/pom.xml b/inject/configdriven/tests/configuredby/pom.xml index 5da89cb08ee..2e24b1c96bf 100644 --- a/inject/configdriven/tests/configuredby/pom.xml +++ b/inject/configdriven/tests/configuredby/pom.xml @@ -22,7 +22,7 @@ io.helidon.inject.configdriven.tests helidon-inject-configdriven-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-inject-configdriven-tests-configuredby Helidon Injection ConfigDriven Tests CfgedBy Svc diff --git a/inject/configdriven/tests/pom.xml b/inject/configdriven/tests/pom.xml index 62fd294186e..1908e865841 100644 --- a/inject/configdriven/tests/pom.xml +++ b/inject/configdriven/tests/pom.xml @@ -24,7 +24,7 @@ io.helidon.inject.configdriven helidon-inject-configdriven-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.inject.configdriven.tests helidon-inject-configdriven-tests-project diff --git a/inject/maven-plugin/pom.xml b/inject/maven-plugin/pom.xml index 20dfd232941..c3e3441e353 100644 --- a/inject/maven-plugin/pom.xml +++ b/inject/maven-plugin/pom.xml @@ -22,7 +22,7 @@ io.helidon.inject helidon-inject-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/maven-plugin/src/main/java/io/helidon/inject/maven/plugin/QualifierConfig.java b/inject/maven-plugin/src/main/java/io/helidon/inject/maven/plugin/QualifierConfig.java index 6753c140f44..454cdcad0e8 100644 --- a/inject/maven-plugin/src/main/java/io/helidon/inject/maven/plugin/QualifierConfig.java +++ b/inject/maven-plugin/src/main/java/io/helidon/inject/maven/plugin/QualifierConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.helidon.inject.maven.plugin; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -76,6 +77,11 @@ public Map values() { return Map.of("value", value); } + @Override + public List metaAnnotations() { + return List.of(); + } + @Override public int compareTo(Annotation o) { return this.typeName().compareTo(o.typeName()); diff --git a/inject/pom.xml b/inject/pom.xml index 3cad3213ec0..1ac13135127 100644 --- a/inject/pom.xml +++ b/inject/pom.xml @@ -24,7 +24,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.inject helidon-inject-project diff --git a/inject/processor/pom.xml b/inject/processor/pom.xml index 9dccec5bf59..7caa3b3e141 100644 --- a/inject/processor/pom.xml +++ b/inject/processor/pom.xml @@ -23,7 +23,7 @@ io.helidon.inject helidon-inject-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/runtime/pom.xml b/inject/runtime/pom.xml index d73740c7b77..c327a4c8503 100644 --- a/inject/runtime/pom.xml +++ b/inject/runtime/pom.xml @@ -22,7 +22,7 @@ io.helidon.inject helidon-inject-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/testing/pom.xml b/inject/testing/pom.xml index 37c57b3d765..2c58bb95514 100644 --- a/inject/testing/pom.xml +++ b/inject/testing/pom.xml @@ -22,7 +22,7 @@ io.helidon.inject helidon-inject-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/tests/api/pom.xml b/inject/tests/api/pom.xml index 1dda2d131be..76dfb672690 100644 --- a/inject/tests/api/pom.xml +++ b/inject/tests/api/pom.xml @@ -23,7 +23,7 @@ io.helidon.inject.tests helidon-inject-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/tests/interception/pom.xml b/inject/tests/interception/pom.xml index 8c0dffb0cb3..017f073b9e6 100644 --- a/inject/tests/interception/pom.xml +++ b/inject/tests/interception/pom.xml @@ -23,7 +23,7 @@ io.helidon.inject.tests helidon-inject-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/tests/pom.xml b/inject/tests/pom.xml index fb5bc295752..3351244402c 100644 --- a/inject/tests/pom.xml +++ b/inject/tests/pom.xml @@ -22,7 +22,7 @@ io.helidon.inject helidon-inject-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/tests/resources-inject/pom.xml b/inject/tests/resources-inject/pom.xml index 06b24a9aaaa..278afb844ca 100644 --- a/inject/tests/resources-inject/pom.xml +++ b/inject/tests/resources-inject/pom.xml @@ -23,7 +23,7 @@ io.helidon.inject.tests helidon-inject-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/tests/resources-plain/pom.xml b/inject/tests/resources-plain/pom.xml index b9705861fa0..cde4ff4090a 100644 --- a/inject/tests/resources-plain/pom.xml +++ b/inject/tests/resources-plain/pom.xml @@ -23,7 +23,7 @@ io.helidon.inject.tests helidon-inject-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/tests/runtime/pom.xml b/inject/tests/runtime/pom.xml index 4c676c20f35..8570a3f22e4 100644 --- a/inject/tests/runtime/pom.xml +++ b/inject/tests/runtime/pom.xml @@ -23,7 +23,7 @@ io.helidon.inject.tests helidon-inject-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/tests/tck-jsr330/pom.xml b/inject/tests/tck-jsr330/pom.xml index 8e809f28029..098f935368d 100644 --- a/inject/tests/tck-jsr330/pom.xml +++ b/inject/tests/tck-jsr330/pom.xml @@ -23,7 +23,7 @@ io.helidon.inject.tests helidon-inject-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/inject/tools/pom.xml b/inject/tools/pom.xml index 5551fb2c22f..c7f415b8a9d 100644 --- a/inject/tools/pom.xml +++ b/inject/tools/pom.xml @@ -23,7 +23,7 @@ io.helidon.inject helidon-inject-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/integrations/cdi/common-cdi/configurable/pom.xml b/integrations/cdi/common-cdi/configurable/pom.xml index 73b676435c5..fe50465e152 100644 --- a/integrations/cdi/common-cdi/configurable/pom.xml +++ b/integrations/cdi/common-cdi/configurable/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-configurable Helidon CDI Integrations Common Configurable diff --git a/integrations/cdi/common-cdi/delegates/pom.xml b/integrations/cdi/common-cdi/delegates/pom.xml index d430042e20e..bbc8f35ffa1 100644 --- a/integrations/cdi/common-cdi/delegates/pom.xml +++ b/integrations/cdi/common-cdi/delegates/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-delegates Helidon CDI Integrations Common Delegates diff --git a/integrations/cdi/common-cdi/pom.xml b/integrations/cdi/common-cdi/pom.xml index a5e6f64a34b..e328ecc9596 100644 --- a/integrations/cdi/common-cdi/pom.xml +++ b/integrations/cdi/common-cdi/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-common-project pom diff --git a/integrations/cdi/common-cdi/reference-counted-context/pom.xml b/integrations/cdi/common-cdi/reference-counted-context/pom.xml index 00f1c1c24ea..e5dc6815274 100644 --- a/integrations/cdi/common-cdi/reference-counted-context/pom.xml +++ b/integrations/cdi/common-cdi/reference-counted-context/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-reference-counted-context Helidon Integrations CDI Common Ref Counted Context diff --git a/integrations/cdi/datasource-hikaricp/pom.xml b/integrations/cdi/datasource-hikaricp/pom.xml index 89e785d3f36..e3bccf4e67c 100644 --- a/integrations/cdi/datasource-hikaricp/pom.xml +++ b/integrations/cdi/datasource-hikaricp/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-datasource-hikaricp Helidon CDI Integrations HikariCP DataSource diff --git a/integrations/cdi/datasource-ucp/pom.xml b/integrations/cdi/datasource-ucp/pom.xml index 21d36266b65..6f49a42a604 100644 --- a/integrations/cdi/datasource-ucp/pom.xml +++ b/integrations/cdi/datasource-ucp/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-datasource-ucp Helidon CDI Integrations UCP DataSource diff --git a/integrations/cdi/datasource/pom.xml b/integrations/cdi/datasource/pom.xml index aeba9874e9d..1ddc8b439be 100644 --- a/integrations/cdi/datasource/pom.xml +++ b/integrations/cdi/datasource/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-datasource Helidon CDI Integrations DataSource diff --git a/integrations/cdi/eclipselink-cdi/pom.xml b/integrations/cdi/eclipselink-cdi/pom.xml index 67363b43d39..81255df19fb 100644 --- a/integrations/cdi/eclipselink-cdi/pom.xml +++ b/integrations/cdi/eclipselink-cdi/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-eclipselink Helidon CDI Integrations Eclipselink diff --git a/integrations/cdi/hibernate-cdi/pom.xml b/integrations/cdi/hibernate-cdi/pom.xml index d6eb41a92b6..b4b0fa6fea2 100644 --- a/integrations/cdi/hibernate-cdi/pom.xml +++ b/integrations/cdi/hibernate-cdi/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-hibernate Helidon CDI Integrations Hibernate diff --git a/integrations/cdi/jpa-cdi/pom.xml b/integrations/cdi/jpa-cdi/pom.xml index 24f2d36c5d6..66e36fd48a4 100644 --- a/integrations/cdi/jpa-cdi/pom.xml +++ b/integrations/cdi/jpa-cdi/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-jpa Helidon CDI Integrations JPA @@ -111,6 +111,11 @@ jakarta.transaction-api provided + + org.eclipse.microprofile.config + microprofile-config-api + provided + @@ -147,6 +152,12 @@ jaxb-impl runtime + + io.helidon.microprofile.config + helidon-microprofile-config + runtime + true + diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaExtension.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaExtension.java index 41b2309b1b5..7841deab068 100644 --- a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaExtension.java +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaExtension.java @@ -101,6 +101,7 @@ import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; +import org.eclipse.microprofile.config.ConfigProvider; import static jakarta.interceptor.Interceptor.Priority.LIBRARY_AFTER; import static jakarta.interceptor.Interceptor.Priority.LIBRARY_BEFORE; @@ -316,7 +317,10 @@ public JpaExtension() { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn); } - this.enabled = Boolean.parseBoolean(System.getProperty(this.getClass().getName() + ".enabled", "false")); + this.enabled = + Boolean.parseBoolean(ConfigProvider.getConfig() + .getOptionalValue(this.getClass().getName() + ".enabled", String.class) + .orElse("false")); if (LOGGER.isLoggable(Level.FINE) && !this.enabled) { LOGGER.logp(Level.FINE, cn, mn, "jpaExtensionDisabled", this.getClass().getName()); } diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceExtension.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceExtension.java index 665a2c1c367..1fe45cc0de3 100644 --- a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceExtension.java +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceExtension.java @@ -34,6 +34,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -74,7 +75,6 @@ import jakarta.enterprise.inject.spi.AnnotatedCallable; import jakarta.enterprise.inject.spi.AnnotatedType; import jakarta.enterprise.inject.spi.Bean; -import jakarta.enterprise.inject.spi.BeanAttributes; import jakarta.enterprise.inject.spi.BeanManager; import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; import jakarta.enterprise.inject.spi.Extension; @@ -107,12 +107,18 @@ import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; +import org.eclipse.microprofile.config.Config; import static jakarta.interceptor.Interceptor.Priority.LIBRARY_AFTER; import static jakarta.interceptor.Interceptor.Priority.LIBRARY_BEFORE; import static jakarta.persistence.PersistenceContextType.EXTENDED; import static jakarta.persistence.SynchronizationType.SYNCHRONIZED; import static jakarta.persistence.SynchronizationType.UNSYNCHRONIZED; +import static java.util.Spliterator.DISTINCT; +import static java.util.Spliterator.IMMUTABLE; +import static java.util.Spliterator.NONNULL; +import static java.util.stream.StreamSupport.stream; +import static org.eclipse.microprofile.config.ConfigProvider.getConfig; /** * An {@link Extension} that integrates container-mode > BEAN_ENTITYMANAGERFACTORY_TYPELITERAL = new TypeLiteral<>() {}; - - private static final TypeLiteral> BEAN_JTAEXTENDEDENTITYMANAGER_TYPELITERAL = - new TypeLiteral<>() {}; - - private static final TypeLiteral> BEAN_JTAENTITYMANAGER_TYPELITERAL = new TypeLiteral<>() {}; - /** * The name used to designate the only persistence unit in the environment, when there is exactly one persistence * unit in the environment, and there is at least one {@link PersistenceContext @PersistenceContext}-annotated @@ -270,7 +269,10 @@ public final class PersistenceExtension implements Extension { @Deprecated // For invocation by CDI only. public PersistenceExtension() { super(); - this.enabled = Boolean.parseBoolean(System.getProperty(this.getClass().getName() + ".enabled", "true")); + this.enabled = + Boolean.parseBoolean(getConfig() + .getOptionalValue(this.getClass().getName() + ".enabled", String.class) + .orElse("true")); this.containerManagedEntityManagerQualifiers = new HashSet<>(); this.containerManagedEntityManagerFactoryQualifiers = new HashSet<>(); this.entityManagerFactoryQualifiers = new HashSet<>(); @@ -587,10 +589,13 @@ private void addSyntheticBeans(@Observes @Priority(LIBRARY_AFTER) AfterBeanDisco // Next, and most commonly, load all META-INF/persistence.xml resources with JAXB, and turn them into // PersistenceUnitInfo instances, and add beans for all of them as well as their associated PersistenceProviders // (if applicable). + String persistenceXmlResourceName = getConfig() + .getOptionalValue("jakarta.persistence.descriptor.resource.name", String.class) + .orElse("META-INF/persistence.xml"); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Enumeration urls; try { - urls = classLoader.getResources("META-INF/persistence.xml"); + urls = classLoader.getResources(persistenceXmlResourceName); } catch (IOException e) { event.addDefinitionError(e); processImplicits = false; @@ -1290,7 +1295,7 @@ private void addContainerManagedEntityManagerFactoryBeans(AfterBeanDiscovery eve .addTransitiveTypeClosure(EntityManagerFactory.class) .scope(Singleton.class) .qualifiers(qualifiers) - .produceWith(PersistenceExtension::produceEntityManagerFactory) + .produceWith(i -> produceEntityManagerFactory(i, qualifiers)) .disposeWith(PersistenceExtension::disposeEntityManagerFactory); } } @@ -1317,8 +1322,8 @@ private void addExtendedEntityManagerBeans(AfterBeanDiscovery event, Set produceJtaExtendedEntityManager(i, qualifiers)) + .disposeWith((em, i) -> disposeJtaExtendedEntityManager(em, i, qualifiers)); } private void addJtaTransactionScopedEntityManagerBeans(AfterBeanDiscovery event, Set suppliedQualifiers) { @@ -1343,8 +1348,8 @@ private void addJtaTransactionScopedEntityManagerBeans(AfterBeanDiscovery event, .addTransitiveTypeClosure(JtaEntityManager.class) .scope(Dependent.class) // critical: must be Dependent scope .qualifiers(qualifiers) - .produceWith(PersistenceExtension::produceJtaEntityManager) - .disposeWith(PersistenceExtension::disposeJtaEntityManager); + .produceWith(i -> produceJtaEntityManager(i, qualifiers)) + .disposeWith((em, i) -> disposeJtaEntityManager(em, i, qualifiers)); } @@ -1353,17 +1358,15 @@ private void addJtaTransactionScopedEntityManagerBeans(AfterBeanDiscovery event, */ - private static JtaExtendedEntityManager produceJtaExtendedEntityManager(Instance instance) { - BeanAttributes ba = instance.select(BEAN_JTAEXTENDEDENTITYMANAGER_TYPELITERAL).get(); + private static JtaExtendedEntityManager produceJtaExtendedEntityManager(Instance instance, + Set beanQualifiers) { Set containerManagedSelectionQualifiers = new HashSet<>(); containerManagedSelectionQualifiers.add(ContainerManaged.Literal.INSTANCE); Set selectionQualifiers = new HashSet<>(); SynchronizationType syncType = null; Map properties = new HashMap<>(); - for (Annotation beanQualifier : ba.getQualifiers()) { - if (beanQualifier == Any.Literal.INSTANCE) { - continue; - } else if (beanQualifier == Unsynchronized.Literal.INSTANCE) { + for (Annotation beanQualifier : beanQualifiers) { + if (beanQualifier == Unsynchronized.Literal.INSTANCE) { if (syncType == null) { syncType = UNSYNCHRONIZED; } @@ -1379,12 +1382,19 @@ private static JtaExtendedEntityManager produceJtaExtendedEntityManager(Instance } } SynchronizationType finalSyncType = syncType == null ? SYNCHRONIZED : syncType; + Config config = getOrDefault(instance.select(Config.class), selectionQualifiers); + Map stackedProperties = + new StackedMap<>(List.of(properties.keySet()::stream, + () -> stream(config.getPropertyNames()::spliterator, + DISTINCT | IMMUTABLE | NONNULL, + false)), + List.of(properties::get, + k -> config.getOptionalValue(k, String.class).orElse(null))); return instance.select(ReferenceCountingProducer.class) .get() .produce(() -> { TransactionRegistry tr = - getOrDefault(instance.select(TransactionRegistry.class), - selectionQualifiers.toArray(EMPTY_ANNOTATION_ARRAY)); + getOrDefault(instance.select(TransactionRegistry.class), selectionQualifiers); return new JtaExtendedEntityManager(tr::active, tr::get, @@ -1393,7 +1403,7 @@ private static JtaExtendedEntityManager produceJtaExtendedEntityManager(Instance containerManagedSelectionQualifiers .toArray(EMPTY_ANNOTATION_ARRAY)) .get() - .createEntityManager(finalSyncType, properties), + .createEntityManager(finalSyncType, stackedProperties), finalSyncType == SYNCHRONIZED); }, JtaExtendedEntityManager::dispose, @@ -1401,9 +1411,11 @@ private static JtaExtendedEntityManager produceJtaExtendedEntityManager(Instance containerManagedSelectionQualifiers); } - private static void disposeJtaExtendedEntityManager(JtaExtendedEntityManager em, Instance instance) { + private static void disposeJtaExtendedEntityManager(JtaExtendedEntityManager em, + Instance instance, + Set beanQualifiers) { Set containerManagedSelectionQualifiers = new HashSet<>(); - for (Annotation beanQualifier : instance.select(BEAN_JTAEXTENDEDENTITYMANAGER_TYPELITERAL).get().getQualifiers()) { + for (Annotation beanQualifier : beanQualifiers) { if (beanQualifier == ContainerManaged.Literal.INSTANCE || beanQualifier != Any.Literal.INSTANCE && !SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { @@ -1424,24 +1436,20 @@ private static PersistenceUnitInfoBean producePersistenceUnitInfoBean(Instance instance) { - BeanAttributes ba = instance.select(BEAN_ENTITYMANAGERFACTORY_TYPELITERAL).get(); + private static EntityManagerFactory produceEntityManagerFactory(Instance instance, + Set beanQualifiers) { Set selectionQualifiers = new HashSet<>(); Set namedSelectionQualifiers = new HashSet<>(); - for (Annotation beanQualifier: ba.getQualifiers()) { - if (beanQualifier == Any.Literal.INSTANCE) { - continue; - } else if (beanQualifier instanceof Named) { + for (Annotation beanQualifier: beanQualifiers) { + if (beanQualifier instanceof Named) { namedSelectionQualifiers.add(beanQualifier); - } else if (!SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { + } else if (beanQualifier != Any.Literal.INSTANCE && !SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { namedSelectionQualifiers.add(beanQualifier); selectionQualifiers.add(beanQualifier); } } - PersistenceUnitInfo pui = - getOrDefault(instance.select(PersistenceUnitInfo.class), namedSelectionQualifiers); - PersistenceProvider pp = - getOrDefault(instance.select(PersistenceProvider.class), selectionQualifiers); + PersistenceUnitInfo pui = getOrDefault(instance.select(PersistenceUnitInfo.class), namedSelectionQualifiers); + PersistenceProvider pp = getOrDefault(instance.select(PersistenceProvider.class), selectionQualifiers); Map properties = new HashMap<>(5); properties.put("jakarta.persistence.bean.manager", instance.select(BeanManager.class).get()); try { @@ -1455,6 +1463,14 @@ private static EntityManagerFactory produceEntityManagerFactory(Instance } catch (final ClassNotFoundException classNotFoundException) { } + Config config = getOrDefault(instance.select(Config.class), namedSelectionQualifiers); + properties = + new StackedMap<>(List.of(properties.keySet()::stream, + () -> stream(config.getPropertyNames()::spliterator, + DISTINCT | IMMUTABLE | NONNULL, + false)), + List.of(properties::get, + k -> config.getOptionalValue(k, String.class).orElse(null))); return pp.createContainerEntityManagerFactory(pui, properties); } @@ -1462,16 +1478,15 @@ private static void disposeEntityManagerFactory(EntityManagerFactory emf, Instan emf.close(); } - private static JtaEntityManager produceJtaEntityManager(Instance instance) { + private static JtaEntityManager produceJtaEntityManager(Instance instance, + Set beanQualifiers) { Set containerManagedSelectionQualifiers = new HashSet<>(); containerManagedSelectionQualifiers.add(ContainerManaged.Literal.INSTANCE); Set selectionQualifiers = new HashSet<>(); SynchronizationType syncType = null; Map properties = new HashMap<>(); - for (Annotation beanQualifier : instance.select(BEAN_JTAENTITYMANAGER_TYPELITERAL).get().getQualifiers()) { - if (beanQualifier == Any.Literal.INSTANCE) { - continue; - } else if (beanQualifier == Unsynchronized.Literal.INSTANCE) { + for (Annotation beanQualifier : beanQualifiers) { + if (beanQualifier == Unsynchronized.Literal.INSTANCE) { if (syncType == null) { syncType = UNSYNCHRONIZED; } @@ -1481,12 +1496,20 @@ private static JtaEntityManager produceJtaEntityManager(Instance instanc containerManagedSelectionQualifiers.add(pp); properties.put(ppName, pp.value()); } - } else if (!SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { + } else if (beanQualifier != Any.Literal.INSTANCE && !SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { containerManagedSelectionQualifiers.add(beanQualifier); selectionQualifiers.add(beanQualifier); } } SynchronizationType finalSyncType = syncType == null ? SYNCHRONIZED : syncType; + Config config = getOrDefault(instance.select(Config.class), selectionQualifiers); + Map stackedProperties = + new StackedMap<>(List.of(properties.keySet()::stream, + () -> stream(config.getPropertyNames()::spliterator, + DISTINCT | IMMUTABLE | NONNULL, + false)), + List.of(properties::get, + k -> config.getOptionalValue(k, String.class).orElse(null))); return instance.select(ReferenceCountingProducer.class) .get() .produce(() -> { @@ -1500,19 +1523,20 @@ private static JtaEntityManager produceJtaEntityManager(Instance instanc containerManagedSelectionQualifiers.toArray(EMPTY_ANNOTATION_ARRAY)) .get()::createEntityManager, finalSyncType, - properties); + stackedProperties); }, JtaEntityManager::dispose, JtaEntityManager.class, containerManagedSelectionQualifiers); } - private static void disposeJtaEntityManager(JtaEntityManager em, Instance instance) { + private static void disposeJtaEntityManager(JtaEntityManager em, + Instance instance, + Set beanQualifiers) { Set containerManagedSelectionQualifiers = new HashSet<>(); - for (Annotation beanQualifier : instance.select(BEAN_JTAENTITYMANAGER_TYPELITERAL).get().getQualifiers()) { + for (Annotation beanQualifier : beanQualifiers) { if (beanQualifier == ContainerManaged.Literal.INSTANCE - || beanQualifier != Any.Literal.INSTANCE - && !SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { + || beanQualifier != Any.Literal.INSTANCE && !SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { containerManagedSelectionQualifiers.add(beanQualifier); } } diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/StackedMap.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/StackedMap.java new file mode 100644 index 00000000000..fe28a19bda5 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/StackedMap.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.cdi.jpa; + +import java.util.AbstractMap; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toUnmodifiableSet; +import static java.util.stream.StreamSupport.stream; + +final class StackedMap extends AbstractMap { + + + /* + * Instance fields. + */ + + + private final Supplier> ks; + + private final Function vf; + + + /* + * Constructors. + */ + + + StackedMap(Iterable>> kss, + Iterable> vfs) { + this(union(kss), vf(vfs)); + } + + StackedMap(Supplier> ks, Function vf) { + super(); + this.ks = Objects.requireNonNull(ks, "ks"); + this.vf = Objects.requireNonNull(vf, "vf"); + } + + + /* + * Instance methods. + */ + + + @Override // AbstractMap + public Set> entrySet() { + return new EntrySet(); + } + + + /* + * Static methods. + */ + + + private static Supplier> union(Iterable>> i) { + Objects.requireNonNull(i, "i"); + return () -> stream(i.spliterator(), false) + .flatMap(Supplier::get) + .collect(toUnmodifiableSet()); + } + + private static Function vf(Iterable> vfs) { + Objects.requireNonNull(vfs, "vfs"); + return k -> stream(vfs.spliterator(), false) + .flatMap(vf -> Optional.ofNullable(vf.apply(k)).stream()) + .findFirst() + .orElse(null); + } + + + /* + * Inner and nested classes. + */ + + + private final class EntrySet extends AbstractSet> { + + + /* + * Constructors. + */ + + + private EntrySet() { + super(); + } + + + /* + * Instance methods. + */ + + + @Override // AbstractSet> + public int size() { + return ks.get().size(); + } + + @Override // AbstractSet> + public Iterator> iterator() { + return new I(); + } + + + /* + * Inner and nested classes. + */ + + + private final class I implements Iterator> { + + + /* + * Instance fields. + */ + + + private final Iterator ki; + + + /* + * Constructors. + */ + + + private I() { + super(); + this.ki = ks.get().iterator(); + } + + + /* + * Instance methods. + */ + + + @Override // Iterator> + public boolean hasNext() { + return this.ki.hasNext(); + } + + @Override // Iterator> + public Entry next() { + K k = this.ki.next(); + return new SimpleImmutableEntry<>(k, vf.apply(k)); + } + + } + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/module-info.java b/integrations/cdi/jpa-cdi/src/main/java/module-info.java index c92a5da4d35..9e0e0133172 100644 --- a/integrations/cdi/jpa-cdi/src/main/java/module-info.java +++ b/integrations/cdi/jpa-cdi/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,13 +28,13 @@ * @see io.helidon.integrations.cdi.jpa.PersistenceUnitInfoBean */ @Feature(value = "JPA", - description = "Jakarta persistence API support for Helidon MP", - in = HelidonFlavor.MP, - path = "JPA" + description = "Jakarta persistence API support for Helidon MP", + in = HelidonFlavor.MP, + path = "JPA" ) -@SuppressWarnings({ "deprecation", "requires-automatic"}) +@SuppressWarnings({ "deprecation", "requires-automatic" }) module io.helidon.integrations.cdi.jpa { - + requires jakarta.xml.bind; requires jakarta.inject; // automatic module @@ -47,8 +47,8 @@ requires transitive jakarta.persistence; // automatic module requires transitive java.sql; - // JTA is optional at runtime, as well as the modules that support - // it. + requires microprofile.config.api; + requires static io.helidon.common.features.api; // Static metamodel generation requires access to java.compiler at diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestAlternatePersistenceXmlLocation.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestAlternatePersistenceXmlLocation.java new file mode 100644 index 00000000000..30c91b8c916 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestAlternatePersistenceXmlLocation.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.cdi.jpa; + +import jakarta.annotation.sql.DataSourceDefinition; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceContext; + +import io.helidon.microprofile.config.ConfigCdiExtension; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class TestAlternatePersistenceXmlLocation { + + private SeContainer sec; + + private TestAlternatePersistenceXmlLocation() { + super(); + } + + @BeforeEach + @SuppressWarnings("unchecked") + final void initializeCdiContainer() { + System.setProperty(JpaExtension.class.getName() + ".enabled", "false"); + System.setProperty(PersistenceExtension.class.getName() + ".enabled", "true"); + System.setProperty("jakarta.persistence.descriptor.resource.name", "META-INF/test-persistence.xml"); + Class cdiSeJtaPlatformClass; + try { + // Load it dynamically because Hibernate won't be on the classpath when we're testing with Eclipselink + cdiSeJtaPlatformClass = + Class.forName("io.helidon.integrations.cdi.hibernate.CDISEJtaPlatform", + false, + Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + cdiSeJtaPlatformClass = null; + } + SeContainerInitializer i = SeContainerInitializer.newInstance() + .disableDiscovery() + .addExtensions(PersistenceExtension.class, + ConfigCdiExtension.class, + com.arjuna.ats.jta.cdi.TransactionExtension.class, + io.helidon.integrations.datasource.hikaricp.cdi.HikariCPBackedDataSourceExtension.class) + .addBeanClasses(Frobnicator.class); + if (cdiSeJtaPlatformClass != null) { + i = i.addBeanClasses(cdiSeJtaPlatformClass); + } + this.sec = i.initialize(); + } + + @AfterEach + final void closeCdiContainer() { + if (this.sec != null) { + this.sec.close(); + } + System.setProperty(PersistenceExtension.class.getName() + ".enabled", "false"); + System.setProperty(JpaExtension.class.getName() + ".enabled", "true"); + System.clearProperty("jakarta.persistence.descriptor.resource.name"); + } + + @Test + final void testAlternatePersistenceXmlLocation() { + Instance fi = sec.select(Frobnicator.class); + Frobnicator f = fi.get(); + assertThat(f.em.isOpen(), is(true)); + fi.destroy(f); + } + + @DataSourceDefinition( + name = "test", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:TestAlternatePersistenceXmlLocation", + serverName = "", + properties = { + "user=sa" + } + ) + @Dependent + private static class Frobnicator { + + @PersistenceContext(unitName = "test-alternate") + private EntityManager em; + + @Inject + Frobnicator() { + super(); + } + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestApplicationAndContainerManagedInjections.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestApplicationAndContainerManagedInjections.java new file mode 100644 index 00000000000..f2965ad859a --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestApplicationAndContainerManagedInjections.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.integrations.cdi.jpa; + +import java.util.Objects; + +import jakarta.annotation.sql.DataSourceDefinition; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.enterprise.inject.Disposes; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceContext; + +import io.helidon.microprofile.config.ConfigCdiExtension; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static jakarta.persistence.Persistence.createEntityManagerFactory; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class TestApplicationAndContainerManagedInjections { + + private SeContainer sec; + + private TestApplicationAndContainerManagedInjections() { + super(); + } + + @BeforeEach + @SuppressWarnings("unchecked") + final void initializeCdiContainer() { + System.setProperty(JpaExtension.class.getName() + ".enabled", "false"); + System.setProperty(PersistenceExtension.class.getName() + ".enabled", "true"); + Class cdiSeJtaPlatformClass; + try { + // Load it dynamically because Hibernate won't be on the classpath when we're testing with Eclipselink + cdiSeJtaPlatformClass = + Class.forName("io.helidon.integrations.cdi.hibernate.CDISEJtaPlatform", + false, + Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + cdiSeJtaPlatformClass = null; + } + SeContainerInitializer i = SeContainerInitializer.newInstance() + .disableDiscovery() + .addExtensions(ConfigCdiExtension.class, + PersistenceExtension.class, + com.arjuna.ats.jta.cdi.TransactionExtension.class, + io.helidon.integrations.datasource.hikaricp.cdi.HikariCPBackedDataSourceExtension.class) + .addBeanClasses(Frobnicator.class); + if (cdiSeJtaPlatformClass != null) { + i = i.addBeanClasses(cdiSeJtaPlatformClass); + } + this.sec = i.initialize(); + } + + @AfterEach + final void closeCdiContainer() { + if (this.sec != null) { + this.sec.close(); + } + System.setProperty(PersistenceExtension.class.getName() + ".enabled", "false"); + System.setProperty(JpaExtension.class.getName() + ".enabled", "true"); + } + + @Test + final void testApplicationAndContainerManagedInjections() { + Instance fi = sec.select(Frobnicator.class); + Frobnicator f = fi.get(); + assertThat(f.containerManagedEm.isOpen(), is(true)); + assertThat(f.containerManagedEm, instanceOf(JtaEntityManager.class)); + assertThat(f.applicationManagedEm.isOpen(), is(true)); + fi.destroy(f); + } + + @DataSourceDefinition( + name = "test", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:TestApplicationAndContainerManagedInjections", + serverName = "", + properties = { + "user=sa" + } + ) + @Dependent + private static class Frobnicator { + + @PersistenceContext(unitName = "test") + private EntityManager containerManagedEm; + + private final EntityManager applicationManagedEm; + + @Inject + Frobnicator(EntityManager applicationManagedEm) { + super(); + this.applicationManagedEm = Objects.requireNonNull(applicationManagedEm); + } + + @Produces + @Singleton + static EntityManagerFactory produceApplicationManagedEntityManagerFactory() { + return createEntityManagerFactory("test-resource-local"); + } + + static void disposeApplicationManagedEntityManagerFactory(@Disposes EntityManagerFactory applicationManagedEmf) { + applicationManagedEmf.close(); + } + + @Produces + @Dependent + static EntityManager produceApplicationManagedEntityManager(EntityManagerFactory applicationManagedEmf) { + return applicationManagedEmf.createEntityManager(); + } + + static void disposeApplicationManagedEntityManager(@Disposes EntityManager applicationManagedEm) { + applicationManagedEm.close(); + } + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestPersistenceExtension.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestPersistenceExtension.java index 6f94ae737e0..50a878fee06 100644 --- a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestPersistenceExtension.java +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestPersistenceExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ */ package io.helidon.integrations.cdi.jpa; +import java.util.Map; + import jakarta.annotation.sql.DataSourceDefinition; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.Dependent; @@ -24,16 +26,21 @@ import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceUnit; + +import io.helidon.microprofile.config.ConfigCdiExtension; +import org.eclipse.microprofile.config.Config; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.fail; @@ -41,6 +48,8 @@ class TestPersistenceExtension { private SeContainer sec; + private boolean hibernate; + private TestPersistenceExtension() { super(); } @@ -60,9 +69,11 @@ final void initializeCdiContainer() { } catch (ClassNotFoundException e) { cdiSeJtaPlatformClass = null; } + this.hibernate = cdiSeJtaPlatformClass != null; SeContainerInitializer i = SeContainerInitializer.newInstance() .disableDiscovery() .addExtensions(PersistenceExtension.class, + ConfigCdiExtension.class, com.arjuna.ats.jta.cdi.TransactionExtension.class, io.helidon.integrations.datasource.hikaricp.cdi.HikariCPBackedDataSourceExtension.class) .addBeanClasses(Caturgiator.class, @@ -82,13 +93,44 @@ final void closeCdiContainer() { System.setProperty(JpaExtension.class.getName() + ".enabled", "true"); } - // @Disabled @Test final void testSpike() { Instance fi = sec.select(Frobnicator.class); Frobnicator f = fi.get(); + assertThat(f.em.isOpen(), is(true)); assertThat(f.em, instanceOf(JtaEntityManager.class)); + Map properties = f.em.getProperties(); + assertThat(properties.containsKey("java.vendor.url"), is(true)); // proves MicroProfile Config integration works + // This is kind of odd. Eclipselink implements EntityManager#getProperties() such that the returned map has + // all discoverable properties: those from the persistence unit, and of course any explicitly specified by + // the user. Hibernate does not: properties starting with "eclipselink.", as an arbitrary example, are not + // present. + assertThat(properties.get("eclipselink.jdbc.native-sql"), this.hibernate ? nullValue() : is("true")); + + assertThat(f.emf.isOpen(), is(true)); + properties = f.emf.getProperties(); + assertThat(properties.containsKey("java.vendor.url"), is(true)); // (proves that MicroProfile Config works) + // Note that this assertion also means that Hibernate's strange properties behavior above does not apply to the + // equivalent scenario involving EntityManagerFactory instances (for no particular reason?). + assertThat(properties.get("eclipselink.jdbc.native-sql"), is("true")); + + // (Setting system properties to a different value does not change anything, because the persistence unit + // properties in META-INF/persistence.xml overrule any other sources.) + String old = System.setProperty("eclipselink.jdbc.native-sql", "false"); + try { + assertThat(properties.get("eclipselink.jdbc.native-sql"), is("true")); + // (Note that Helidon's MicroProfile Config implementation reflects the change.) + assertThat(sec.select(Config.class).get() + .getOptionalValue("eclipselink.jdbc.native-sql", String.class).orElse(null), + is("false")); + } finally { + if (old == null) { + System.clearProperty("eclipselink.jdbc.native-sql"); + } else { + System.setProperty("eclipselink.jdbc.native-sql", old); + } + } Instance ci = sec.select(Caturgiator.class); Caturgiator c = ci.get(); @@ -111,6 +153,9 @@ final void testSpike() { @Dependent private static class Frobnicator { + @PersistenceUnit(unitName = "test") + private EntityManagerFactory emf; + @PersistenceContext(unitName = "test") private EntityManager em; diff --git a/integrations/cdi/jpa-cdi/src/test/resources/META-INF/persistence.xml b/integrations/cdi/jpa-cdi/src/test/resources/META-INF/persistence.xml index e1698885042..f81c39f1381 100644 --- a/integrations/cdi/jpa-cdi/src/test/resources/META-INF/persistence.xml +++ b/integrations/cdi/jpa-cdi/src/test/resources/META-INF/persistence.xml @@ -1,7 +1,7 @@ + + + + A persistence unit for use by the unit test that tests an alternate descriptor location. + test + + + + + + + + + + + + + diff --git a/integrations/cdi/jta-cdi/pom.xml b/integrations/cdi/jta-cdi/pom.xml index d86d47f9a56..1a2839681e1 100644 --- a/integrations/cdi/jta-cdi/pom.xml +++ b/integrations/cdi/jta-cdi/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-jta Helidon CDI Integrations JTA diff --git a/integrations/cdi/jta-weld/pom.xml b/integrations/cdi/jta-weld/pom.xml index 1c328127205..926037ea59a 100644 --- a/integrations/cdi/jta-weld/pom.xml +++ b/integrations/cdi/jta-weld/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations.cdi helidon-integrations-cdi-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-cdi-jta-weld Helidon CDI Integrations JTA Weld diff --git a/integrations/cdi/pom.xml b/integrations/cdi/pom.xml index 7f9902a57c5..458afedd538 100644 --- a/integrations/cdi/pom.xml +++ b/integrations/cdi/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.cdi helidon-integrations-cdi-project diff --git a/integrations/common/pom.xml b/integrations/common/pom.xml index 0d3fef1e4c8..88385e889a3 100644 --- a/integrations/common/pom.xml +++ b/integrations/common/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/integrations/common/rest/pom.xml b/integrations/common/rest/pom.xml index 56d15f75e4a..d2635351e68 100644 --- a/integrations/common/rest/pom.xml +++ b/integrations/common/rest/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.common helidon-integrations-common-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-common-rest diff --git a/integrations/db/h2/pom.xml b/integrations/db/h2/pom.xml index d4aa275c391..b39c9b99b92 100644 --- a/integrations/db/h2/pom.xml +++ b/integrations/db/h2/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.db helidon-integrations-db-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT h2 diff --git a/integrations/db/mysql/pom.xml b/integrations/db/mysql/pom.xml index c81e372d30e..19132f61d71 100644 --- a/integrations/db/mysql/pom.xml +++ b/integrations/db/mysql/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.db helidon-integrations-db-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-db-mysql diff --git a/integrations/db/ojdbc/pom.xml b/integrations/db/ojdbc/pom.xml index 0140eb5cf33..832b55ab9ff 100644 --- a/integrations/db/ojdbc/pom.xml +++ b/integrations/db/ojdbc/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.db helidon-integrations-db-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ojdbc diff --git a/integrations/db/pgsql/pom.xml b/integrations/db/pgsql/pom.xml index 67b3369c624..728fb90c09a 100644 --- a/integrations/db/pgsql/pom.xml +++ b/integrations/db/pgsql/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.db helidon-integrations-db-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-db-pgsql diff --git a/integrations/db/pom.xml b/integrations/db/pom.xml index 32def2e0f07..d3c9bbac8a5 100644 --- a/integrations/db/pom.xml +++ b/integrations/db/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 pom diff --git a/integrations/graal/mp-native-image-extension/pom.xml b/integrations/graal/mp-native-image-extension/pom.xml index 29af6547340..0f34c7ff846 100644 --- a/integrations/graal/mp-native-image-extension/pom.xml +++ b/integrations/graal/mp-native-image-extension/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations.graal helidon-integrations-graal-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/integrations/graal/native-image-extension/pom.xml b/integrations/graal/native-image-extension/pom.xml index 976be58c6a2..096f5a6a65f 100644 --- a/integrations/graal/native-image-extension/pom.xml +++ b/integrations/graal/native-image-extension/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations.graal helidon-integrations-graal-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/integrations/graal/pom.xml b/integrations/graal/pom.xml index bdc0a0e65fb..1ce679e439d 100644 --- a/integrations/graal/pom.xml +++ b/integrations/graal/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT diff --git a/integrations/jdbc/jdbc/pom.xml b/integrations/jdbc/jdbc/pom.xml index 3694f69b14e..77e53ea0cef 100644 --- a/integrations/jdbc/jdbc/pom.xml +++ b/integrations/jdbc/jdbc/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations helidon-integrations-jdbc-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.jdbc diff --git a/integrations/jdbc/pom.xml b/integrations/jdbc/pom.xml index ac7e11d9691..1043a62c5cf 100644 --- a/integrations/jdbc/pom.xml +++ b/integrations/jdbc/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 pom diff --git a/integrations/jta/jdbc/pom.xml b/integrations/jta/jdbc/pom.xml index 8f924d9a54c..396a30f7881 100644 --- a/integrations/jta/jdbc/pom.xml +++ b/integrations/jta/jdbc/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations helidon-integrations-jta-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.jta diff --git a/integrations/jta/pom.xml b/integrations/jta/pom.xml index a0963fac22a..8df186e6adb 100644 --- a/integrations/jta/pom.xml +++ b/integrations/jta/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 pom diff --git a/integrations/micrometer/cdi/pom.xml b/integrations/micrometer/cdi/pom.xml index 69afbc3fa53..9ef22fa5fb2 100644 --- a/integrations/micrometer/cdi/pom.xml +++ b/integrations/micrometer/cdi/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations helidon-integrations-micrometer-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.micrometer diff --git a/integrations/micrometer/micrometer/pom.xml b/integrations/micrometer/micrometer/pom.xml index e496c5dc9ed..fa6b41d67f9 100644 --- a/integrations/micrometer/micrometer/pom.xml +++ b/integrations/micrometer/micrometer/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations helidon-integrations-micrometer-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.micrometer @@ -49,6 +49,10 @@ io.micrometer micrometer-registry-prometheus + + io.micrometer + micrometer-registry-prometheus-simpleclient + io.prometheus simpleclient diff --git a/integrations/micrometer/micrometer/src/main/java/module-info.java b/integrations/micrometer/micrometer/src/main/java/module-info.java index 798330a40f6..03b8a9a0d94 100644 --- a/integrations/micrometer/micrometer/src/main/java/module-info.java +++ b/integrations/micrometer/micrometer/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ requires transitive io.helidon.common; requires transitive io.helidon.servicecommon; + requires micrometer.registry.prometheus.simpleclient; exports io.helidon.integrations.micrometer; diff --git a/integrations/micrometer/pom.xml b/integrations/micrometer/pom.xml index 5dada09cb5a..a721b041682 100644 --- a/integrations/micrometer/pom.xml +++ b/integrations/micrometer/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-micrometer-project Helidon Integrations Micrometer Project diff --git a/integrations/micronaut/cdi-processor/pom.xml b/integrations/micronaut/cdi-processor/pom.xml index 8eac75ce44e..e33cf5663dc 100644 --- a/integrations/micronaut/cdi-processor/pom.xml +++ b/integrations/micronaut/cdi-processor/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.micronaut helidon-integrations-micronaut-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-micronaut-cdi-processor diff --git a/integrations/micronaut/cdi/pom.xml b/integrations/micronaut/cdi/pom.xml index db36aec526b..d86c10dd4cd 100644 --- a/integrations/micronaut/cdi/pom.xml +++ b/integrations/micronaut/cdi/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.micronaut helidon-integrations-micronaut-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-micronaut-cdi diff --git a/integrations/micronaut/data/pom.xml b/integrations/micronaut/data/pom.xml index a101f282531..faa9b9e9c80 100644 --- a/integrations/micronaut/data/pom.xml +++ b/integrations/micronaut/data/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.micronaut helidon-integrations-micronaut-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-micronaut-data diff --git a/integrations/micronaut/pom.xml b/integrations/micronaut/pom.xml index c4aa6057eb1..6d86576f8cb 100644 --- a/integrations/micronaut/pom.xml +++ b/integrations/micronaut/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.micronaut diff --git a/integrations/microstream/cache/pom.xml b/integrations/microstream/cache/pom.xml index b5a4c58e6c4..6eadeefe723 100644 --- a/integrations/microstream/cache/pom.xml +++ b/integrations/microstream/cache/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.microstream helidon-integrations-microstream-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-microstream-cache diff --git a/integrations/microstream/cdi/pom.xml b/integrations/microstream/cdi/pom.xml index 42130fc2081..d7ec2b6f11d 100644 --- a/integrations/microstream/cdi/pom.xml +++ b/integrations/microstream/cdi/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.microstream helidon-integrations-microstream-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-microstream-cdi diff --git a/integrations/microstream/core/pom.xml b/integrations/microstream/core/pom.xml index d4cfad884ae..498a743bd74 100644 --- a/integrations/microstream/core/pom.xml +++ b/integrations/microstream/core/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations.microstream helidon-integrations-microstream-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-microstream diff --git a/integrations/microstream/health/pom.xml b/integrations/microstream/health/pom.xml index 665f6ff6bce..de22bd8b847 100644 --- a/integrations/microstream/health/pom.xml +++ b/integrations/microstream/health/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.microstream helidon-integrations-microstream-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-microstream-health diff --git a/integrations/microstream/metrics/pom.xml b/integrations/microstream/metrics/pom.xml index 3bc5944b8c3..9b4b4d16397 100644 --- a/integrations/microstream/metrics/pom.xml +++ b/integrations/microstream/metrics/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.microstream helidon-integrations-microstream-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-microstream-metrics diff --git a/integrations/microstream/pom.xml b/integrations/microstream/pom.xml index 4009d16bb7d..cbed6978b57 100644 --- a/integrations/microstream/pom.xml +++ b/integrations/microstream/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.microstream diff --git a/integrations/neo4j/health/pom.xml b/integrations/neo4j/health/pom.xml index c40cc968cc7..2e693ccba8f 100644 --- a/integrations/neo4j/health/pom.xml +++ b/integrations/neo4j/health/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations.neo4j helidon-integrations-neo4j-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-neo4j-health diff --git a/integrations/neo4j/metrics/pom.xml b/integrations/neo4j/metrics/pom.xml index 01b6380e4df..b536aa82f27 100644 --- a/integrations/neo4j/metrics/pom.xml +++ b/integrations/neo4j/metrics/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.neo4j helidon-integrations-neo4j-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-neo4j-metrics diff --git a/integrations/neo4j/neo4j/pom.xml b/integrations/neo4j/neo4j/pom.xml index b1eca13ba98..b1c25adcfc3 100644 --- a/integrations/neo4j/neo4j/pom.xml +++ b/integrations/neo4j/neo4j/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations.neo4j helidon-integrations-neo4j-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-neo4j diff --git a/integrations/neo4j/pom.xml b/integrations/neo4j/pom.xml index 7730325d7f8..f0080d3f136 100644 --- a/integrations/neo4j/pom.xml +++ b/integrations/neo4j/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.neo4j helidon-integrations-neo4j-project diff --git a/integrations/oci/authentication/instance/pom.xml b/integrations/oci/authentication/instance/pom.xml index 44a0de4db4a..a5a9a60304d 100644 --- a/integrations/oci/authentication/instance/pom.xml +++ b/integrations/oci/authentication/instance/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations.oci.authentication helidon-integrations-oci-authentication-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-oci-authentication-instance Helidon Integrations OCI Authentication Instance diff --git a/integrations/oci/authentication/oke-workload/pom.xml b/integrations/oci/authentication/oke-workload/pom.xml index 35957e2203e..e27d78354a8 100644 --- a/integrations/oci/authentication/oke-workload/pom.xml +++ b/integrations/oci/authentication/oke-workload/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations.oci.authentication helidon-integrations-oci-authentication-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-oci-authentication-oke-workload Helidon Integrations OCI Authentication Workload diff --git a/integrations/oci/authentication/pom.xml b/integrations/oci/authentication/pom.xml index ab0aa0721c6..00bff83e513 100644 --- a/integrations/oci/authentication/pom.xml +++ b/integrations/oci/authentication/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations.oci helidon-integrations-oci-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/integrations/oci/authentication/resource/pom.xml b/integrations/oci/authentication/resource/pom.xml index b1effbe2924..58016e4a4d6 100644 --- a/integrations/oci/authentication/resource/pom.xml +++ b/integrations/oci/authentication/resource/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations.oci.authentication helidon-integrations-oci-authentication-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-oci-authentication-resource Helidon Integrations OCI Resource Instance diff --git a/integrations/oci/metrics/cdi/pom.xml b/integrations/oci/metrics/cdi/pom.xml index 2b0b12bfdcd..07acd0c166f 100644 --- a/integrations/oci/metrics/cdi/pom.xml +++ b/integrations/oci/metrics/cdi/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.oci.metrics helidon-integrations-oci-metrics-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-oci-metrics-cdi Helidon Integrations OCI Metrics CDI diff --git a/integrations/oci/metrics/metrics/pom.xml b/integrations/oci/metrics/metrics/pom.xml index 3ce96c3abd4..d2053bfb7de 100644 --- a/integrations/oci/metrics/metrics/pom.xml +++ b/integrations/oci/metrics/metrics/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.oci.metrics helidon-integrations-oci-metrics-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-oci-metrics Helidon Integrations OCI Metrics diff --git a/integrations/oci/metrics/pom.xml b/integrations/oci/metrics/pom.xml index 56c96a012e3..d2d47ac1e91 100644 --- a/integrations/oci/metrics/pom.xml +++ b/integrations/oci/metrics/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.oci helidon-integrations-oci-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.oci.metrics helidon-integrations-oci-metrics-project diff --git a/integrations/oci/oci/pom.xml b/integrations/oci/oci/pom.xml index ced67f7c12b..47d482e5e8a 100644 --- a/integrations/oci/oci/pom.xml +++ b/integrations/oci/oci/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations.oci helidon-integrations-oci-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-oci Helidon Integrations OCI diff --git a/integrations/oci/pom.xml b/integrations/oci/pom.xml index 16126200f48..cb52bf38317 100644 --- a/integrations/oci/pom.xml +++ b/integrations/oci/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.oci diff --git a/integrations/oci/sdk/cdi/pom.xml b/integrations/oci/sdk/cdi/pom.xml index d1b216d8a13..8a511a64595 100644 --- a/integrations/oci/sdk/cdi/pom.xml +++ b/integrations/oci/sdk/cdi/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations.oci.sdk helidon-integrations-oci-sdk-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-oci-sdk-cdi Helidon Integrations OCI SDK CDI diff --git a/integrations/oci/sdk/pom.xml b/integrations/oci/sdk/pom.xml index 6a2bffe69aa..a352bd19437 100644 --- a/integrations/oci/sdk/pom.xml +++ b/integrations/oci/sdk/pom.xml @@ -24,7 +24,7 @@ io.helidon.integrations.oci helidon-integrations-oci-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.integrations.oci.sdk diff --git a/integrations/oci/sdk/processor/pom.xml b/integrations/oci/sdk/processor/pom.xml index 8ccb3f242d8..51c8e6fecbe 100644 --- a/integrations/oci/sdk/processor/pom.xml +++ b/integrations/oci/sdk/processor/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.oci.sdk helidon-integrations-oci-sdk-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/integrations/oci/sdk/runtime/pom.xml b/integrations/oci/sdk/runtime/pom.xml index a6964ab8c9f..ef259f9d52f 100644 --- a/integrations/oci/sdk/runtime/pom.xml +++ b/integrations/oci/sdk/runtime/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.oci.sdk helidon-integrations-oci-sdk-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/integrations/oci/sdk/tests/pom.xml b/integrations/oci/sdk/tests/pom.xml index 1d7c8931d27..39ca6fab5a0 100644 --- a/integrations/oci/sdk/tests/pom.xml +++ b/integrations/oci/sdk/tests/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.oci.sdk helidon-integrations-oci-sdk-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/integrations/oci/sdk/tests/test-application/pom.xml b/integrations/oci/sdk/tests/test-application/pom.xml index 6bee3b11c37..8a80b8ae6ac 100644 --- a/integrations/oci/sdk/tests/test-application/pom.xml +++ b/integrations/oci/sdk/tests/test-application/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.oci.sdk.tests helidon-integrations-oci-sdk-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/integrations/oci/sdk/tests/test-module1/pom.xml b/integrations/oci/sdk/tests/test-module1/pom.xml index dd095622004..19c06e81fd5 100644 --- a/integrations/oci/sdk/tests/test-module1/pom.xml +++ b/integrations/oci/sdk/tests/test-module1/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.oci.sdk.tests helidon-integrations-oci-sdk-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/integrations/oci/sdk/tests/test-module2/pom.xml b/integrations/oci/sdk/tests/test-module2/pom.xml index b187f19bd98..dd9d1c503eb 100644 --- a/integrations/oci/sdk/tests/test-module2/pom.xml +++ b/integrations/oci/sdk/tests/test-module2/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.oci.sdk.tests helidon-integrations-oci-sdk-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/integrations/oci/secrets-config-source/pom.xml b/integrations/oci/secrets-config-source/pom.xml index 56c7bd287b3..b45103104a7 100644 --- a/integrations/oci/secrets-config-source/pom.xml +++ b/integrations/oci/secrets-config-source/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations.oci helidon-integrations-oci-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-oci-secrets-config-source Helidon Integrations OCI Secrets Config Source diff --git a/integrations/oci/secrets-mp-config-source/pom.xml b/integrations/oci/secrets-mp-config-source/pom.xml index 937a8b40e9e..8672dab50a5 100644 --- a/integrations/oci/secrets-mp-config-source/pom.xml +++ b/integrations/oci/secrets-mp-config-source/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations.oci helidon-integrations-oci-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-oci-secrets-mp-config-source Helidon Integrations OCI Secrets MP Config Source diff --git a/integrations/oci/tests/authentication/pom.xml b/integrations/oci/tests/authentication/pom.xml index 6b12b95f338..935d8748bae 100644 --- a/integrations/oci/tests/authentication/pom.xml +++ b/integrations/oci/tests/authentication/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations.oci.tests helidon-integrations-oci-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-oci-tests-authentication diff --git a/integrations/oci/tests/pom.xml b/integrations/oci/tests/pom.xml index 652c1aa3bfb..702b46f1557 100644 --- a/integrations/oci/tests/pom.xml +++ b/integrations/oci/tests/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations.oci helidon-integrations-oci-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.integrations.oci.tests diff --git a/integrations/oci/tls-certificates/pom.xml b/integrations/oci/tls-certificates/pom.xml index da9f580880e..6e584f9932a 100644 --- a/integrations/oci/tls-certificates/pom.xml +++ b/integrations/oci/tls-certificates/pom.xml @@ -23,7 +23,7 @@ io.helidon.integrations.oci helidon-integrations-oci-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/integrations/openapi-ui/pom.xml b/integrations/openapi-ui/pom.xml index eac0f1ab174..4c5ec48fd84 100644 --- a/integrations/openapi-ui/pom.xml +++ b/integrations/openapi-ui/pom.xml @@ -21,7 +21,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.openapi-ui diff --git a/integrations/pom.xml b/integrations/pom.xml index 5175ab424d9..a89501efbeb 100644 --- a/integrations/pom.xml +++ b/integrations/pom.xml @@ -24,7 +24,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations helidon-integrations-project diff --git a/integrations/vault/auths/approle/pom.xml b/integrations/vault/auths/approle/pom.xml index d3af08ca900..a220c77a02d 100644 --- a/integrations/vault/auths/approle/pom.xml +++ b/integrations/vault/auths/approle/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault.auths helidon-integrations-vault-auths-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault-auths-approle diff --git a/integrations/vault/auths/common/pom.xml b/integrations/vault/auths/common/pom.xml index 6d40fcd3286..6d636185e3c 100644 --- a/integrations/vault/auths/common/pom.xml +++ b/integrations/vault/auths/common/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault.auths helidon-integrations-vault-auths-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault-auths-common diff --git a/integrations/vault/auths/k8s/pom.xml b/integrations/vault/auths/k8s/pom.xml index f91876580e4..ba298d0e3c2 100644 --- a/integrations/vault/auths/k8s/pom.xml +++ b/integrations/vault/auths/k8s/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault.auths helidon-integrations-vault-auths-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault-auths-k8s diff --git a/integrations/vault/auths/pom.xml b/integrations/vault/auths/pom.xml index 8816fadc6ea..ff4a6219336 100644 --- a/integrations/vault/auths/pom.xml +++ b/integrations/vault/auths/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault helidon-integrations-vault-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/integrations/vault/auths/token/pom.xml b/integrations/vault/auths/token/pom.xml index cfd22aafaa1..f501f894b55 100644 --- a/integrations/vault/auths/token/pom.xml +++ b/integrations/vault/auths/token/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault.auths helidon-integrations-vault-auths-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault-auths-token diff --git a/integrations/vault/cdi/pom.xml b/integrations/vault/cdi/pom.xml index c39dfa2a9cf..79b059783db 100644 --- a/integrations/vault/cdi/pom.xml +++ b/integrations/vault/cdi/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault helidon-integrations-vault-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault-cdi diff --git a/integrations/vault/pom.xml b/integrations/vault/pom.xml index ea3cdcd84d7..cc19f339936 100644 --- a/integrations/vault/pom.xml +++ b/integrations/vault/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations helidon-integrations-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.integrations.vault diff --git a/integrations/vault/secrets/cubbyhole/pom.xml b/integrations/vault/secrets/cubbyhole/pom.xml index 9c22f7f8e98..1414eb67f9b 100644 --- a/integrations/vault/secrets/cubbyhole/pom.xml +++ b/integrations/vault/secrets/cubbyhole/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault.secrets helidon-integrations-vault-secrets-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault-secrets-cubbyhole diff --git a/integrations/vault/secrets/database/pom.xml b/integrations/vault/secrets/database/pom.xml index 846f638e126..744eb057f0a 100644 --- a/integrations/vault/secrets/database/pom.xml +++ b/integrations/vault/secrets/database/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault.secrets helidon-integrations-vault-secrets-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault-secrets-database diff --git a/integrations/vault/secrets/kv1/pom.xml b/integrations/vault/secrets/kv1/pom.xml index 099f361c9ee..bf9bb6367ee 100644 --- a/integrations/vault/secrets/kv1/pom.xml +++ b/integrations/vault/secrets/kv1/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault.secrets helidon-integrations-vault-secrets-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault-secrets-kv1 diff --git a/integrations/vault/secrets/kv2/pom.xml b/integrations/vault/secrets/kv2/pom.xml index 0d826ebf2d1..1da8f0489bc 100644 --- a/integrations/vault/secrets/kv2/pom.xml +++ b/integrations/vault/secrets/kv2/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault.secrets helidon-integrations-vault-secrets-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault-secrets-kv2 diff --git a/integrations/vault/secrets/pki/pom.xml b/integrations/vault/secrets/pki/pom.xml index 7070ca59f75..2729cafdd2c 100644 --- a/integrations/vault/secrets/pki/pom.xml +++ b/integrations/vault/secrets/pki/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault.secrets helidon-integrations-vault-secrets-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault-secrets-pki diff --git a/integrations/vault/secrets/pom.xml b/integrations/vault/secrets/pom.xml index f99dd58d038..0c1111f8075 100644 --- a/integrations/vault/secrets/pom.xml +++ b/integrations/vault/secrets/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault helidon-integrations-vault-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/integrations/vault/secrets/transit/pom.xml b/integrations/vault/secrets/transit/pom.xml index b95f528726c..713dc3515dd 100644 --- a/integrations/vault/secrets/transit/pom.xml +++ b/integrations/vault/secrets/transit/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault.secrets helidon-integrations-vault-secrets-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault-secrets-transit diff --git a/integrations/vault/sys/pom.xml b/integrations/vault/sys/pom.xml index 404f35651d2..b32615438b6 100644 --- a/integrations/vault/sys/pom.xml +++ b/integrations/vault/sys/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault helidon-integrations-vault-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/integrations/vault/sys/sys/pom.xml b/integrations/vault/sys/sys/pom.xml index 26382c0bce0..4824a5ab8ee 100644 --- a/integrations/vault/sys/sys/pom.xml +++ b/integrations/vault/sys/sys/pom.xml @@ -21,7 +21,7 @@ helidon-integrations-vault-sys-project io.helidon.integrations.vault.sys - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/integrations/vault/vault/pom.xml b/integrations/vault/vault/pom.xml index 0949545b738..8b74bb3e8f4 100644 --- a/integrations/vault/vault/pom.xml +++ b/integrations/vault/vault/pom.xml @@ -22,7 +22,7 @@ io.helidon.integrations.vault helidon-integrations-vault-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-integrations-vault diff --git a/jersey/client/pom.xml b/jersey/client/pom.xml index 7707bdc9d35..9a830cac4ec 100644 --- a/jersey/client/pom.xml +++ b/jersey/client/pom.xml @@ -21,7 +21,7 @@ io.helidon.jersey helidon-jersey-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/jersey/common/pom.xml b/jersey/common/pom.xml index aa2b2faac33..847ddd14872 100644 --- a/jersey/common/pom.xml +++ b/jersey/common/pom.xml @@ -21,7 +21,7 @@ helidon-jersey-project io.helidon.jersey - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/jersey/connector/pom.xml b/jersey/connector/pom.xml index 8406160726c..67ace39606b 100644 --- a/jersey/connector/pom.xml +++ b/jersey/connector/pom.xml @@ -21,7 +21,7 @@ helidon-jersey-project io.helidon.jersey - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java index 59c83627782..0481b9031a1 100644 --- a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java +++ b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java @@ -56,6 +56,7 @@ import static io.helidon.jersey.connector.HelidonProperties.SHARE_CONNECTION_CACHE; import static io.helidon.jersey.connector.HelidonProperties.TLS; import static org.glassfish.jersey.client.ClientProperties.CONNECT_TIMEOUT; +import static org.glassfish.jersey.client.ClientProperties.EXPECT_100_CONTINUE; import static org.glassfish.jersey.client.ClientProperties.FOLLOW_REDIRECTS; import static org.glassfish.jersey.client.ClientProperties.READ_TIMEOUT; import static org.glassfish.jersey.client.ClientProperties.getValue; @@ -76,10 +77,11 @@ class HelidonConnector implements Connector { private final WebClient webClient; private final Proxy proxy; + @SuppressWarnings("unchecked") HelidonConnector(Client client, Configuration config) { // create underlying HTTP client Map properties = config.getProperties(); - var builder = WebClientConfig.builder(); + WebClientConfig.Builder builder = WebClientConfig.builder(); // use config for client Config helidonConfig = helidonConfig(config).orElse(Config.empty()); @@ -98,14 +100,17 @@ class HelidonConnector implements Connector { if (properties.containsKey(FOLLOW_REDIRECTS)) { builder.followRedirects(getValue(properties, FOLLOW_REDIRECTS, true)); } + if (properties.containsKey(EXPECT_100_CONTINUE)) { + builder.sendExpectContinue(getValue(properties, EXPECT_100_CONTINUE, true)); + } - //Whether WebClient TLS has been already set via config + // whether WebClient TLS has been already set via config boolean helidonConfigTlsSet = helidonConfig.map(hc -> hc.get("tls").exists()).orElse(false); boolean isJerseyClient = client instanceof JerseyClient; - //Whether Jersey client has non-default SslContext set. If so, we should honor these settings + // whether Jersey client has non-default SslContext set. If so, we should honor these settings boolean jerseyHasDefaultSsl = isJerseyClient && ((JerseyClient) client).isDefaultSslContext(); - if (!helidonConfigTlsSet || !isJerseyClient || !jerseyHasDefaultSsl) {// prefer Tls over SSLContext + if (!helidonConfigTlsSet || !isJerseyClient || !jerseyHasDefaultSsl) { // prefer Tls over SSLContext if (properties.containsKey(TLS)) { builder.tls(getValue(properties, TLS, Tls.class)); } else if (client.getSslContext() != null) { diff --git a/jersey/connector/src/test/java/io/helidon/jersey/connector/ConfigTest.java b/jersey/connector/src/test/java/io/helidon/jersey/connector/ConfigTest.java index 56701f80136..20629316e1e 100644 --- a/jersey/connector/src/test/java/io/helidon/jersey/connector/ConfigTest.java +++ b/jersey/connector/src/test/java/io/helidon/jersey/connector/ConfigTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ void testConfigPropertyOverride() { } @Test - void testConfigDefaut() { + void testConfigDefault() { Client client = ClientBuilder.newClient(); HelidonConnector connector = new HelidonConnector(client, client.getConfiguration()); assertThat(connector.proxy(), is(Proxy.create())); @@ -91,4 +91,20 @@ void testConfigProxy() { assertThat(connector.proxy().username(), is(Optional.of("user"))); assertThat(connector.proxy().password(), notNullValue()); } + + @Test + void testConfig100ContinueDefault() { + Client client = ClientBuilder.newBuilder().build(); + HelidonConnector connector = new HelidonConnector(client, client.getConfiguration()); + assertThat(connector.client().prototype().sendExpectContinue(), is(true)); + } + + @Test + void testConfig100ContinueOverride() { + Client client = ClientBuilder.newBuilder() + .property(ClientProperties.EXPECT_100_CONTINUE, "false") + .build(); + HelidonConnector connector = new HelidonConnector(client, client.getConfiguration()); + assertThat(connector.client().prototype().sendExpectContinue(), is(false)); + } } diff --git a/jersey/jsonp/pom.xml b/jersey/jsonp/pom.xml index abf9ee63144..c383850a790 100644 --- a/jersey/jsonp/pom.xml +++ b/jersey/jsonp/pom.xml @@ -21,7 +21,7 @@ helidon-jersey-project io.helidon.jersey - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/jersey/pom.xml b/jersey/pom.xml index bebc32ddc40..ff0ce4db5fa 100644 --- a/jersey/pom.xml +++ b/jersey/pom.xml @@ -21,7 +21,7 @@ helidon-project io.helidon - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 pom diff --git a/jersey/server/pom.xml b/jersey/server/pom.xml index 2cb489bb706..e23e68463ab 100644 --- a/jersey/server/pom.xml +++ b/jersey/server/pom.xml @@ -22,7 +22,7 @@ io.helidon.jersey helidon-jersey-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-jersey-server diff --git a/jersey/tests/connector/pom.xml b/jersey/tests/connector/pom.xml index efaa6303f11..8db6799319f 100644 --- a/jersey/tests/connector/pom.xml +++ b/jersey/tests/connector/pom.xml @@ -21,7 +21,7 @@ io.helidon.jersey.tests helidon-jersey-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-jersey-tests-connector diff --git a/jersey/tests/pom.xml b/jersey/tests/pom.xml index cd60264fdcb..658e1c6b1b2 100644 --- a/jersey/tests/pom.xml +++ b/jersey/tests/pom.xml @@ -24,7 +24,7 @@ io.helidon.jersey helidon-jersey-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.jersey.tests diff --git a/licensing/pom.xml b/licensing/pom.xml index b9bea0cc9d5..c3698900100 100644 --- a/licensing/pom.xml +++ b/licensing/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.licensing helidon-licensing diff --git a/logging/common/pom.xml b/logging/common/pom.xml index 6bdd030476f..0253140da69 100644 --- a/logging/common/pom.xml +++ b/logging/common/pom.xml @@ -21,7 +21,7 @@ helidon-logging-project io.helidon.logging - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-logging-common diff --git a/logging/jul/etc/spotbugs/exclude.xml b/logging/jul/etc/spotbugs/exclude.xml index 369f312280c..20297c179a4 100644 --- a/logging/jul/etc/spotbugs/exclude.xml +++ b/logging/jul/etc/spotbugs/exclude.xml @@ -1,7 +1,7 @@ - + diff --git a/logging/jul/pom.xml b/logging/jul/pom.xml index bf589f75022..01d1d328eba 100644 --- a/logging/jul/pom.xml +++ b/logging/jul/pom.xml @@ -20,7 +20,7 @@ helidon-logging-project io.helidon.logging - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 @@ -44,6 +44,10 @@ io.helidon.common helidon-common + + io.helidon.metadata + helidon-metadata-hson + org.junit.jupiter junit-jupiter-api @@ -54,6 +58,11 @@ hamcrest-all test + + io.helidon.common.testing + helidon-common-testing-junit5 + test + diff --git a/logging/jul/src/main/java/io/helidon/logging/jul/HelidonConsoleHandler.java b/logging/jul/src/main/java/io/helidon/logging/jul/HelidonConsoleHandler.java index 4ca8d088ac3..27f89fb6249 100644 --- a/logging/jul/src/main/java/io/helidon/logging/jul/HelidonConsoleHandler.java +++ b/logging/jul/src/main/java/io/helidon/logging/jul/HelidonConsoleHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package io.helidon.logging.jul; import java.util.logging.Level; +import java.util.logging.LogManager; import java.util.logging.LogRecord; import java.util.logging.StreamHandler; @@ -35,9 +36,15 @@ public class HelidonConsoleHandler extends StreamHandler { * . */ public HelidonConsoleHandler() { + super(); setOutputStream(System.out); - setLevel(Level.ALL); // Handlers should not filter, loggers should - setFormatter(new HelidonFormatter()); + if (LogManager.getLogManager().getProperty(HelidonConsoleHandler.class.getName() + ".level") == null) { + setLevel(Level.ALL); // Handlers should not filter, loggers should + } + // only set this if none set + if (LogManager.getLogManager().getProperty(HelidonConsoleHandler.class.getName() + ".formatter") == null) { + setFormatter(new HelidonFormatter()); + } } @Override diff --git a/logging/jul/src/main/java/io/helidon/logging/jul/HelidonFormatter.java b/logging/jul/src/main/java/io/helidon/logging/jul/HelidonFormatter.java index 6f1680223ba..3faa127b25d 100644 --- a/logging/jul/src/main/java/io/helidon/logging/jul/HelidonFormatter.java +++ b/logging/jul/src/main/java/io/helidon/logging/jul/HelidonFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,12 +36,13 @@ * It also supports replacement of {@code "!thread!"} with the current thread. */ public class HelidonFormatter extends SimpleFormatter { - private static final String THREAD = "thread"; - private static final String THREAD_TOKEN = "!" + THREAD + "!"; - private static final Pattern THREAD_PATTERN = Pattern.compile(THREAD_TOKEN); - private static final Pattern X_VALUE = Pattern.compile("(\\s?%X\\{)(\\S*?)(})"); - private static final Map PATTERN_CACHE = new HashMap<>(); - private static final String JUL_FORMAT_PROP_KEY = "java.util.logging.SimpleFormatter.format"; + static final String THREAD = "thread"; + static final String THREAD_TOKEN = "!" + THREAD + "!"; + static final Pattern THREAD_PATTERN = Pattern.compile(THREAD_TOKEN); + static final Pattern X_VALUE = Pattern.compile("(\\s?%X\\{)(\\S*?)(})"); + static final Map PATTERN_CACHE = new HashMap<>(); + static final String JUL_FORMAT_PROP_KEY = "java.util.logging.SimpleFormatter.format"; + private final String format = LogManager.getLogManager().getProperty(JUL_FORMAT_PROP_KEY); private final Set parsedProps = new HashSet<>(); private final boolean thread; @@ -57,18 +58,10 @@ public HelidonFormatter() { } } - @Override - public String format(LogRecord record) { - String message = thread ? thread() : format; - for (String parsedKey : parsedProps) { - String value = HelidonMdc.get(parsedKey).orElse(""); - message = PATTERN_CACHE.computeIfAbsent(parsedKey, key -> Pattern.compile("%X\\{" + key + "}")) - .matcher(message).replaceAll(value); - } - return formatRow(record, message); - } - - private String thread() { + /* + Replace the thread pattern in the format with the current thread value + */ + static String thread(String format) { String currentThread = Thread.currentThread().toString(); String message = PATTERN_CACHE.computeIfAbsent(THREAD, key -> Pattern.compile("%X\\{" + THREAD + "}")) .matcher(format).replaceAll(currentThread); @@ -76,9 +69,11 @@ private String thread() { return message; } - //Copied from SimpleFormatter - private String formatRow(LogRecord record, String format) { - ZonedDateTime zdt = ZonedDateTime.ofInstant( + /* + All parameters expected by simple formatter (and json formatter as well) + */ + static Object[] parameters(LogRecord record, String formattedMessage) { + var timestamp = ZonedDateTime.ofInstant( record.getInstant(), ZoneId.systemDefault()); String source; if (record.getSourceClassName() != null) { @@ -89,7 +84,7 @@ private String formatRow(LogRecord record, String format) { } else { source = record.getLoggerName(); } - String message = formatMessage(record); + String throwable = ""; if (record.getThrown() != null) { StringWriter sw = new StringWriter(); @@ -99,12 +94,31 @@ private String formatRow(LogRecord record, String format) { pw.close(); throwable = sw.toString(); } + + Object[] result = new Object[6]; + result[0] = timestamp; + result[1] = source; + result[2] = record.getLoggerName(); + result[3] = record.getLevel().getName(); + result[4] = formattedMessage; + result[5] = throwable; + + return result; + } + + @Override + public String format(LogRecord record) { + String message = thread ? thread(format) : format; + for (String parsedKey : parsedProps) { + String value = HelidonMdc.get(parsedKey).orElse(""); + message = PATTERN_CACHE.computeIfAbsent(parsedKey, key -> Pattern.compile("%X\\{" + key + "}")) + .matcher(message).replaceAll(value); + } + return formatRow(record, message); + } + + private String formatRow(LogRecord record, String format) { return String.format(format, - zdt, - source, - record.getLoggerName(), - record.getLevel().getLocalizedName(), - message, - throwable); + parameters(record, super.formatMessage(record))); } } diff --git a/logging/jul/src/main/java/io/helidon/logging/jul/HelidonJsonFormatter.java b/logging/jul/src/main/java/io/helidon/logging/jul/HelidonJsonFormatter.java new file mode 100644 index 00000000000..868c8c1b605 --- /dev/null +++ b/logging/jul/src/main/java/io/helidon/logging/jul/HelidonJsonFormatter.java @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.logging.jul; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Formatter; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.logging.common.HelidonMdc; +import io.helidon.metadata.hson.Hson; + +import static io.helidon.logging.jul.HelidonFormatter.JUL_FORMAT_PROP_KEY; +import static io.helidon.logging.jul.HelidonFormatter.THREAD_TOKEN; + +/** + * A {@link java.util.logging.Formatter} that stores each log record as a single-line JSON value. + * It also replaces all occurrences of MDC tags like {@code %X{value}} with specific values, + * and supports replacement of {@code "!thread!"} with the current thread. + *

+ * The configuration should be done through property {@value #JSON_FORMAT_PROP_KEY}, that provides comma separated list of name to + * fields to log (references are the same as you would use in a {@link java.util.logging.SimpleFormatter}). + *

+ * The configuration falls back to property {@value io.helidon.logging.jul.HelidonFormatter#JUL_FORMAT_PROP_KEY}, + * and analyzes it to provide a "guessed" JSON structure. + *

+ * Example (and also the default format):
+ * {@value #DEFAULT_FORMAT} + */ +public class HelidonJsonFormatter extends Formatter { + static final String DEFAULT_FORMAT = "ts:%1$tQ,date:%1$tY.%1$tm.%1$td,time:%1$tH:%1$tM:%1$tS.%1$tL,level:%4$s,message:%5$s," + + "exception:%6$s,thread:!thread!,logger:%3$s"; + + private static final String JSON_FORMAT_PROP_KEY = "io.helidon.logging.jul.HelidonJsonFormatter.fields"; + + // formats we understand + private static final String EPOCH_MILLIS_FORMAT = "%1$tQ"; + private static final String YEAR_FORMAT = "%1$tY"; + private static final String MONTH_FORMAT = "%1$tm"; + private static final String DAY_FORMAT = "%1$td"; + private static final String HOUR_FORMAT = "%1$tH"; + private static final String MINUTE_FORMAT = "%1$tM"; + private static final String SECOND_FORMAT = "%1$tS"; + private static final String SOURCE_FORMAT = "%2$s"; + private static final String LOGGER_FORMAT = "%3$s"; + private static final String LEVEL_FORMAT = "%4$s"; + private static final String MESSAGE_FORMAT = "%5$s"; + private static final String EXCEPTION_FORMAT = "%6$s"; + + private final List formatters; + + /** + * Create new instance of the {@link io.helidon.logging.jul.HelidonJsonFormatter}. + */ + public HelidonJsonFormatter() { + String jsonFormat = LogManager.getLogManager().getProperty(JSON_FORMAT_PROP_KEY); + String julFormat = LogManager.getLogManager().getProperty(JUL_FORMAT_PROP_KEY); + if (jsonFormat == null && julFormat == null) { + jsonFormat = DEFAULT_FORMAT; + } + if (jsonFormat == null) { + this.formatters = guessFromSimpleFormat(julFormat); + } else { + this.formatters = fromJsonFormat(jsonFormat); + } + } + + HelidonJsonFormatter(String format, boolean jsonFormat) { + this.formatters = jsonFormat ? fromJsonFormat(format) : guessFromSimpleFormat(format); + } + + @Override + public String format(LogRecord record) { + var builder = Hson.Struct.builder(); + var params = HelidonFormatter.parameters(record, super.formatMessage(record)); + + formatters.forEach(formatter -> formatter.update(builder, params)); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + builder.build() + .write(pw); + pw.println(); + pw.close(); + return sw.toString(); + } + + private List fromJsonFormat(String jsonFormat) { + return Stream.of(jsonFormat.split(",")) + .map(it -> new ValueFormatter(Field.create(jsonFormat, it))) + .collect(Collectors.toUnmodifiableList()); + } + + private List guessFromSimpleFormat(String julFormat) { + List result = new ArrayList<>(); + Map counters = new HashMap<>(); + String usedFormat = julFormat.replaceAll("%n", " "); + // spaces expected to separate "blocks" + for (String block : usedFormat.split(" ")) { + if (block.isBlank()) { + continue; + } + // now lets do some "magic" + if (block.contains(YEAR_FORMAT) && block.contains(MONTH_FORMAT) && block.contains(DAY_FORMAT)) { + if (block.contains(HOUR_FORMAT)) { + // full timestamp + result.add(new ValueFormatter(new Field(name(counters, "timestamp"), block))); + } else { + // date only + result.add(new ValueFormatter(new Field(name(counters, "date"), block))); + } + continue; + } + if (block.contains(HOUR_FORMAT) && block.contains(MINUTE_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "time"), block))); + continue; + } + + // now only create sections if it only contains one parameter + if (block.contains(SOURCE_FORMAT) && block.length() < 7) { + result.add(new ValueFormatter(new Field(name(counters, "source"), block))); + continue; + } + if (block.contains(LOGGER_FORMAT) && block.length() < 7) { + result.add(new ValueFormatter(new Field(name(counters, "logger"), block))); + continue; + } + if (block.contains(LEVEL_FORMAT) && block.length() < 7) { + result.add(new ValueFormatter(new Field(name(counters, "level"), block))); + continue; + } + if (block.contains(MESSAGE_FORMAT) && block.length() < 7) { + result.add(new ValueFormatter(new Field(name(counters, "message"), block))); + continue; + } + if (block.contains(EXCEPTION_FORMAT) && block.length() < 7) { + result.add(new ValueFormatter(new Field(name(counters, "exception"), block))); + continue; + } + if (block.contains(THREAD_TOKEN) && block.length() < THREAD_TOKEN.length() + 3) { + result.add(new ValueFormatter(new Field(name(counters, "thread"), block))); + continue; + } + + // now let's extract the parts + if (block.contains(EPOCH_MILLIS_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "ts"), EPOCH_MILLIS_FORMAT))); + } + if (block.contains(YEAR_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "year"), YEAR_FORMAT))); + } + if (block.contains(MONTH_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "month"), MONTH_FORMAT))); + } + if (block.contains(DAY_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "day"), DAY_FORMAT))); + } + if (block.contains(HOUR_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "hour"), HOUR_FORMAT))); + } + if (block.contains(MINUTE_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "minute"), MINUTE_FORMAT))); + } + if (block.contains(SECOND_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "second"), SECOND_FORMAT))); + } + if (block.contains(SOURCE_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "source"), SOURCE_FORMAT))); + } + if (block.contains(LOGGER_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "logger"), LOGGER_FORMAT))); + } + if (block.contains(LEVEL_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "level"), LEVEL_FORMAT))); + } + if (block.contains(MESSAGE_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "message"), MESSAGE_FORMAT))); + } + if (block.contains(EXCEPTION_FORMAT)) { + result.add(new ValueFormatter(new Field(name(counters, "exception"), EXCEPTION_FORMAT))); + } + if (block.contains(THREAD_TOKEN) || block.contains("%X{" + HelidonFormatter.THREAD + "}")) { + result.add(new ValueFormatter(new Field(name(counters, "thread"), THREAD_TOKEN))); + } + + // MDC support + Matcher matcher = HelidonFormatter.X_VALUE.matcher(usedFormat); + while (matcher.find()) { + String name = matcher.group(2); + if (!name.equals(HelidonFormatter.THREAD)) { + result.add(new ValueFormatter(new Field(name(counters, "X." + name), "%X{" + name + "}"))); + } + } + } + return result; + } + + private String name(Map counters, String name) { + if (counters.containsKey(name)) { + return name + "_" + counters.get(name).incrementAndGet(); + } else { + counters.put(name, new AtomicInteger()); + return name; + } + } + + private static class ValueFormatter { + private final Set parsedProps = new HashSet<>(); + private final String jsonName; + private final String format; + private final boolean thread; + + private ValueFormatter(Field field) { + this.jsonName = field.name(); + this.format = field.format(); + + this.thread = this.format.contains(THREAD_TOKEN) || this.format.contains("%X{" + HelidonFormatter.THREAD + "}"); + Matcher matcher = HelidonFormatter.X_VALUE.matcher(this.format); + while (matcher.find()) { + parsedProps.add(matcher.group(2)); + } + } + + private void update(Hson.Struct.Builder jsonBuilder, Object... parameters) { + + String message = thread ? HelidonFormatter.thread(format) : format; + for (String parsedKey : parsedProps) { + String value = HelidonMdc.get(parsedKey).orElse(""); + message = HelidonFormatter.PATTERN_CACHE + .computeIfAbsent(parsedKey, key -> Pattern.compile("%X\\{" + key + "}")) + .matcher(message) + .replaceAll(value); + } + String formattedValue = String.format(message, parameters); + if (!formattedValue.isBlank()) { + jsonBuilder.set(jsonName, formattedValue); + } + } + } + + private record Field(String name, String format) { + private static Field create(String format, String field) { + int index = field.indexOf(':'); + if (index == -1) { + throw new IllegalArgumentException("Invalid format definition for " + HelidonJsonFormatter.class.getSimpleName() + + ", each field must have field name followed by a colon with field " + + "value," + + " such as 'message:%5$s', but got: '" + field + "'. " + + "Full format: " + format); + } + return new Field(field.substring(0, index), field.substring(index + 1)); + } + } +} diff --git a/logging/jul/src/main/java/module-info.java b/logging/jul/src/main/java/module-info.java index e17f91fd07c..36facfd2116 100644 --- a/logging/jul/src/main/java/module-info.java +++ b/logging/jul/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,13 @@ module io.helidon.logging.jul { requires io.helidon.common; + requires io.helidon.metadata.hson; requires java.logging; requires transitive io.helidon.common.context; requires transitive io.helidon.logging.common; - exports io.helidon.logging.jul; + exports io.helidon.logging.jul; provides io.helidon.common.context.spi.DataPropagationProvider with io.helidon.logging.jul.JulMdcPropagator; provides io.helidon.logging.common.spi.MdcProvider with io.helidon.logging.jul.JulMdcProvider; diff --git a/logging/jul/src/test/java/io/helidon/logging/jul/JulJsonTest.java b/logging/jul/src/test/java/io/helidon/logging/jul/JulJsonTest.java new file mode 100644 index 00000000000..9183bcb66b1 --- /dev/null +++ b/logging/jul/src/test/java/io/helidon/logging/jul/JulJsonTest.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.logging.jul; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import io.helidon.logging.common.HelidonMdc; +import io.helidon.metadata.hson.Hson; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class JulJsonTest { + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + private static final Instant INSTANT = Instant.parse("2007-12-03T10:15:30.00Z"); + private static final ZonedDateTime DATE_TIME = INSTANT.atZone(ZoneId.systemDefault()); + + private static final LogRecord LOG_RECORD; + private static final LogRecord LOG_RECORD_WITH_EXCEPTION; + + static { + LogRecord record = new LogRecord(Level.WARNING, "Message content"); + record.setInstant(INSTANT); + record.setLoggerName("LoggerName"); + record.setThrown(new IllegalStateException("Thrown")); + record.setSourceClassName("io.helidon.logging.jul.JulJsonTest"); + record.setSourceMethodName("testJsonDefaultFormat"); + LOG_RECORD_WITH_EXCEPTION = record; + + record = new LogRecord(Level.WARNING, "Message content"); + record.setInstant(INSTANT); + record.setLoggerName("LoggerName"); + record.setSourceClassName("io.helidon.logging.jul.JulJsonTest"); + record.setSourceMethodName("testJsonDefaultFormat"); + LOG_RECORD = record; + } + + @Test + public void testJsonDefaultFormat() throws InterruptedException { + HelidonJsonFormatter formatter = new HelidonJsonFormatter(HelidonJsonFormatter.DEFAULT_FORMAT, true); + + String threadName = "logging-jul-test-thread"; + + AtomicReference resultReference = new AtomicReference<>(); + Thread.ofVirtual() + .name(threadName) + .start(() -> { + resultReference.set(formatter.format(LOG_RECORD)); + }) + .join(Duration.ofSeconds(5)); + + String result = resultReference.get(); + var json = Hson.parse(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8))) + .asStruct(); + + assertThat(json.stringValue("ts"), optionalValue(is(String.valueOf(INSTANT.toEpochMilli())))); + assertThat(json.stringValue("date"), optionalValue(is(DATE_FORMAT.format(DATE_TIME)))); + assertThat(json.stringValue("time"), optionalValue(is(TIME_FORMAT.format(DATE_TIME)))); + assertThat(json.stringValue("level"), optionalValue(is("WARNING"))); + assertThat(json.stringValue("message"), optionalValue(is("Message content"))); + assertThat(json.stringValue("exception"), optionalEmpty()); + assertThat(json.stringValue("logger"), optionalValue(is("LoggerName"))); + assertThat(json.stringValue("thread"), optionalValue(containsString(threadName))); + } + + @Test + public void testJsonWithExceptionDefaultFormat() throws InterruptedException { + HelidonJsonFormatter formatter = new HelidonJsonFormatter(HelidonJsonFormatter.DEFAULT_FORMAT, true); + + String threadName = "logging-jul-test-thread"; + + AtomicReference resultReference = new AtomicReference<>(); + Thread.ofVirtual() + .name(threadName) + .start(() -> { + resultReference.set(formatter.format(LOG_RECORD_WITH_EXCEPTION)); + }) + .join(Duration.ofSeconds(1000)); + + String result = resultReference.get(); + var json = Hson.parse(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8))) + .asStruct(); + + assertThat(json.stringValue("date"), optionalValue(is(DATE_FORMAT.format(DATE_TIME)))); + assertThat(json.stringValue("time"), optionalValue(is(TIME_FORMAT.format(DATE_TIME)))); + assertThat(json.stringValue("level"), optionalValue(is("WARNING"))); + assertThat(json.stringValue("message"), optionalValue(is("Message content"))); + assertThat(json.stringValue("exception"), optionalValue(containsString("Thrown"))); + assertThat(json.stringValue("logger"), optionalValue(is("LoggerName"))); + assertThat(json.stringValue("thread"), optionalValue(containsString(threadName))); + } + + @Test + public void testJsonWithExceptionSimpleFormat() throws InterruptedException { + String simpleFormat = "%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %3$s !thread!: %5$s%6$s %X{test}%n"; + HelidonJsonFormatter formatter = new HelidonJsonFormatter(simpleFormat, false); + + String threadName = "logging-jul-test-thread"; + + AtomicReference resultReference = new AtomicReference<>(); + Thread.ofVirtual() + .name(threadName) + .start(() -> { + HelidonMdc.set("test", "testValue"); + resultReference.set(formatter.format(LOG_RECORD_WITH_EXCEPTION)); + }) + .join(Duration.ofSeconds(1000)); + + String result = resultReference.get(); + var json = Hson.parse(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8))) + .asStruct(); + + assertThat(json.stringValue("date"), optionalValue(is(DATE_FORMAT.format(DATE_TIME)))); + assertThat(json.stringValue("time"), optionalValue(is(TIME_FORMAT.format(DATE_TIME)))); + assertThat(json.stringValue("logger"), optionalValue(is("LoggerName"))); + assertThat(json.stringValue("level"), optionalValue(is("WARNING"))); + assertThat(json.stringValue("thread"), optionalValue(containsString(threadName))); + assertThat(json.stringValue("message"), optionalValue(is("Message content"))); + assertThat(json.stringValue("exception"), optionalValue(containsString("Thrown"))); + assertThat(json.stringValue("X.test"), optionalValue(containsString("testValue"))); + } + + @Test + public void testJsonWithExceptionCustomFormat() throws InterruptedException { + String format = "timestamp:%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL,loglevel:%4$s,test:%X{test},msg:%5$s,source:%2$s"; + HelidonJsonFormatter formatter = new HelidonJsonFormatter(format, true); + + String threadName = "logging-jul-test-thread"; + + AtomicReference resultReference = new AtomicReference<>(); + Thread.ofVirtual() + .name(threadName) + .start(() -> { + HelidonMdc.set("test", "testValue"); + resultReference.set(formatter.format(LOG_RECORD_WITH_EXCEPTION)); + }) + .join(Duration.ofSeconds(1000)); + + String result = resultReference.get(); + var json = Hson.parse(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8))) + .asStruct(); + + assertThat(json.stringValue("timestamp"), optionalValue(is(DATE_FORMAT.format(DATE_TIME) + " " + + TIME_FORMAT.format(DATE_TIME)))); + assertThat(json.stringValue("loglevel"), optionalValue(is("WARNING"))); + assertThat(json.stringValue("msg"), optionalValue(is("Message content"))); + assertThat(json.stringValue("test"), optionalValue(is("testValue"))); + assertThat(json.stringValue("source"), optionalValue(is("io.helidon.logging.jul.JulJsonTest" + + " testJsonDefaultFormat"))); + } +} + diff --git a/logging/log4j/pom.xml b/logging/log4j/pom.xml index dd2f7ea6b7d..9a878517f33 100644 --- a/logging/log4j/pom.xml +++ b/logging/log4j/pom.xml @@ -20,7 +20,7 @@ helidon-logging-project io.helidon.logging - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/logging/pom.xml b/logging/pom.xml index 0a3cacfcb38..056c6597f7e 100644 --- a/logging/pom.xml +++ b/logging/pom.xml @@ -21,7 +21,7 @@ helidon-project io.helidon - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.logging diff --git a/logging/slf4j/pom.xml b/logging/slf4j/pom.xml index 4f99fc4ea33..f763bbc6eed 100644 --- a/logging/slf4j/pom.xml +++ b/logging/slf4j/pom.xml @@ -21,7 +21,7 @@ helidon-logging-project io.helidon.logging - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-logging-slf4j diff --git a/logging/tests/log4j/pom.xml b/logging/tests/log4j/pom.xml index 1b666b7430a..01be9ffe90d 100644 --- a/logging/tests/log4j/pom.xml +++ b/logging/tests/log4j/pom.xml @@ -21,7 +21,7 @@ io.helidon.logging.tests helidon-logging-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-logging-tests-log4j diff --git a/logging/tests/pom.xml b/logging/tests/pom.xml index 197b433b9fb..d3662f04646 100644 --- a/logging/tests/pom.xml +++ b/logging/tests/pom.xml @@ -24,7 +24,7 @@ io.helidon.logging helidon-logging-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.logging.tests diff --git a/lra/coordinator/client/narayana-client/pom.xml b/lra/coordinator/client/narayana-client/pom.xml index a1e8964b0c0..644ec9763c2 100644 --- a/lra/coordinator/client/narayana-client/pom.xml +++ b/lra/coordinator/client/narayana-client/pom.xml @@ -24,7 +24,7 @@ io.helidon.lra helidon-lra-coordinator-client-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-lra-coordinator-narayana-client diff --git a/lra/coordinator/client/narayana-client/src/main/java/io/helidon/lra/coordinator/client/narayana/NarayanaClient.java b/lra/coordinator/client/narayana-client/src/main/java/io/helidon/lra/coordinator/client/narayana/NarayanaClient.java index ae2f0a20fc5..df03a34535f 100644 --- a/lra/coordinator/client/narayana-client/src/main/java/io/helidon/lra/coordinator/client/narayana/NarayanaClient.java +++ b/lra/coordinator/client/narayana-client/src/main/java/io/helidon/lra/coordinator/client/narayana/NarayanaClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,8 @@ import org.eclipse.microprofile.lra.annotation.LRAStatus; import org.eclipse.microprofile.lra.annotation.ws.rs.LRA; +import static java.lang.System.Logger.Level.DEBUG; + /** * Narayana LRA coordinator client. */ @@ -103,6 +105,7 @@ private URI startInternal(URI parentLRA, String clientID, PropagatedHeaders head .map(p -> parseBaseUri(p.toASCIIString())) .orElse(coordinatorUriSupplier.get()); + logF("Starting LRA, coordinator: {0}/start, clientId: {1}, timeout: {2}", baseUri, clientID, timeout); return retry.invoke(() -> { HttpClientRequest req = prepareWebClient(baseUri) .post() @@ -139,6 +142,7 @@ private URI startInternal(URI parentLRA, String clientID, PropagatedHeaders head @Override public void cancel(URI lraId, PropagatedHeaders headers) { + logF("Cancelling LRA {0}", lraId); retry.invoke(() -> { var req = prepareWebClient(lraId) .put() @@ -164,6 +168,7 @@ public void cancel(URI lraId, PropagatedHeaders headers) { @Override public void close(URI lraId, PropagatedHeaders headers) { + logF("Closing LRA {0}", lraId); retry.invoke(() -> { var req = prepareWebClient(lraId) .put() @@ -197,6 +202,7 @@ public Optional join(URI lraId, Participant p) { String links = compensatorLinks(p); + logF("Joining LRA {0} with links: {1}", lraId, links); return retry.invoke(() -> { var req = prepareWebClient(lraId) .put() @@ -227,13 +233,14 @@ public Optional join(URI lraId, throw connectionError("Unexpected coordinator response ", res.status().code()); } } catch (Exception e) { - throw connectionError("Unable to join LRA", e); + throw connectionError("Unable to join LRA " + lraId, e); } }); } @Override public void leave(URI lraId, PropagatedHeaders headers, Participant p) { + logF("Leaving LRA {0} participant: {1}", lraId, p); retry.invoke(() -> { var req = prepareWebClient(lraId) .put() @@ -260,6 +267,7 @@ public void leave(URI lraId, PropagatedHeaders headers, Participant p) { @Override public LRAStatus status(URI lraId, PropagatedHeaders headers) { + logF("Checking status of LRA {0}", lraId); return retry.invoke(() -> { var req = prepareWebClient(lraId) .get() @@ -354,7 +362,9 @@ private CoordinatorConnectionException connectionError(String message, Throwable } private void logF(String msg, Object... params) { - LOGGER.log(Level.DEBUG, msg, params); + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, msg, params); + } } } diff --git a/lra/coordinator/client/pom.xml b/lra/coordinator/client/pom.xml index 0ff6735e772..bb7c82025c0 100644 --- a/lra/coordinator/client/pom.xml +++ b/lra/coordinator/client/pom.xml @@ -24,7 +24,7 @@ io.helidon.lra helidon-lra-coordinator-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.lra helidon-lra-coordinator-client-project diff --git a/lra/coordinator/client/spi/pom.xml b/lra/coordinator/client/spi/pom.xml index 8470fc2ba28..36abc06aa24 100644 --- a/lra/coordinator/client/spi/pom.xml +++ b/lra/coordinator/client/spi/pom.xml @@ -24,7 +24,7 @@ io.helidon.lra helidon-lra-coordinator-client-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-lra-coordinator-client-spi diff --git a/lra/coordinator/pom.xml b/lra/coordinator/pom.xml index a4bea92f804..56b3ed18670 100644 --- a/lra/coordinator/pom.xml +++ b/lra/coordinator/pom.xml @@ -24,7 +24,7 @@ io.helidon.lra helidon-lra-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.lra helidon-lra-coordinator-project diff --git a/lra/coordinator/server/pom.xml b/lra/coordinator/server/pom.xml index 0c801defe74..015c333267b 100644 --- a/lra/coordinator/server/pom.xml +++ b/lra/coordinator/server/pom.xml @@ -23,25 +23,32 @@ https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - io.helidon.applications - helidon-se - 4.1.0-SNAPSHOT - ../../../applications/se/pom.xml + io.helidon.lra + helidon-lra-coordinator-project + 4.2.0-SNAPSHOT - io.helidon.lra helidon-lra-coordinator-server Helidon LRA Coordinator Narayana compatible LRA coordinator - true - true - true io.helidon.lra.coordinator.Main + + + + io.helidon.applications + helidon-se + ${project.parent.version} + pom + import + + + + org.eclipse.microprofile.lra @@ -131,6 +138,120 @@ + ${project.artifactId} + + + + kr.motd.maven + os-maven-plugin + ${version.plugin.os} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${version.plugin.compiler} + + full + + + + org.apache.maven.plugins + maven-surefire-plugin + ${version.plugin.surefire} + + false + + ${project.build.outputDirectory}/logging.properties + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.plugin.failsafe} + + false + true + + + + org.apache.maven.plugins + maven-dependency-plugin + ${version.plugin.dependency} + + + copy-libs + prepare-package + + copy-dependencies + + + ${project.build.directory}/libs + false + false + true + true + runtime + + okio + + + + + + org.apache.maven.plugins + maven-resources-plugin + ${version.plugin.resources} + + + org.apache.maven.plugins + maven-jar-plugin + ${version.plugin.jar} + + + + true + libs + + ${mainClass} + false + + + + + + org.codehaus.mojo + exec-maven-plugin + ${version.plugin.exec} + + java + true + + -classpath + + ${mainClass} + + + + + io.helidon.build-tools + helidon-maven-plugin + ${version.plugin.helidon} + + + io.helidon.licensing + helidon-licensing + ${helidon.version} + + + + + + org.apache.maven.plugins diff --git a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/CoordinatorService.java b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/CoordinatorService.java index bebdf1d0b89..5636870ecf8 100644 --- a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/CoordinatorService.java +++ b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/CoordinatorService.java @@ -95,23 +95,23 @@ public class CoordinatorService implements HttpService { init(); } - private void init() { - lraPersistentRegistry.load(this); - recoveryTask = Scheduling.fixedRate() - .delay(config.get("recovery-interval").asLong().orElse(200L)) - .initialDelay(200) - .timeUnit(TimeUnit.MILLISECONDS) - .task(this::tick) + /** + * Create a new Lra coordinator. + * + * @return coordinator + */ + public static CoordinatorService create() { + return builder() .build(); + } - if (config.get("periodical-persist").asBoolean().orElse(false)) { - persistTask = Scheduling.fixedRate() - .delay(config.get("persist-interval").asLong().orElse(5000L)) - .initialDelay(200) - .timeUnit(TimeUnit.MILLISECONDS) - .task(inv -> lraPersistentRegistry.save()) - .build(); - } + /** + * Create a new fluent API builder. + * + * @return a new builder + */ + public static Builder builder() { + return new Builder(); } /** @@ -149,6 +149,39 @@ public void routing(HttpRules rules) { .put("/{LraId}/remove", this::leave); } + /** + * Get LRA by lraId. + * + * @param lraId without coordinator uri prefix + * @return LRA when managed by this coordinator or null + */ + public Lra lra(String lraId) { + return this.lraPersistentRegistry.get(lraId); + } + + LazyValue coordinatorURL() { + return coordinatorURL; + } + + private void init() { + lraPersistentRegistry.load(this); + recoveryTask = Scheduling.fixedRateBuilder() + .delay(config.get("recovery-interval").asLong().orElse(200L)) + .initialDelay(200) + .timeUnit(TimeUnit.MILLISECONDS) + .task(this::tick) + .build(); + + if (config.get("periodical-persist").asBoolean().orElse(false)) { + persistTask = Scheduling.fixedRateBuilder() + .delay(config.get("persist-interval").asLong().orElse(5000L)) + .initialDelay(200) + .timeUnit(TimeUnit.MILLISECONDS) + .task(inv -> lraPersistentRegistry.save()) + .build(); + } + } + /** * Ask coordinator to start new LRA and return its id. * @@ -163,15 +196,15 @@ private void start(ServerRequest req, ServerResponse res) { String lraUUID = UUID.randomUUID().toString(); URI lraId = coordinatorUriWithPath(lraUUID); if (!parentLRA.isEmpty()) { - Lra parent = lraPersistentRegistry.get(parentLRA.replace(coordinatorURL.get().toASCIIString() + "/", "")); + LraImpl parent = lraPersistentRegistry.get(parentLRA.replace(coordinatorURL.get().toASCIIString() + "/", "")); if (parent != null) { - Lra childLra = new Lra(this, lraUUID, URI.create(parentLRA), this.config); + LraImpl childLra = new LraImpl(this, lraUUID, URI.create(parentLRA), this.config); childLra.setupTimeout(timeLimit); lraPersistentRegistry.put(lraUUID, childLra); parent.addChild(childLra); } } else { - Lra newLra = new Lra(this, lraUUID, config); + LraImpl newLra = new LraImpl(this, lraUUID, config); newLra.setupTimeout(timeLimit); lraPersistentRegistry.put(lraUUID, newLra); } @@ -189,12 +222,12 @@ private void start(ServerRequest req, ServerResponse res) { */ private void close(ServerRequest req, ServerResponse res) { String lraId = req.path().pathParameters().get("LraId"); - Lra lra = lraPersistentRegistry.get(lraId); + LraImpl lra = lraPersistentRegistry.get(lraId); if (lra == null) { res.status(NOT_FOUND_404).send(); return; } - if (lra.status().get() != LRAStatus.Active) { + if (lra.lraStatus().get() != LRAStatus.Active) { // Already time-outed res.status(GONE_410).send(); return; @@ -211,7 +244,7 @@ private void close(ServerRequest req, ServerResponse res) { */ private void cancel(ServerRequest req, ServerResponse res) { String lraId = req.path().pathParameters().get("LraId"); - Lra lra = lraPersistentRegistry.get(lraId); + LraImpl lra = lraPersistentRegistry.get(lraId); if (lra == null) { res.status(NOT_FOUND_404).send(); return; @@ -231,7 +264,7 @@ private void join(ServerRequest req, ServerResponse res) { String lraId = req.path().pathParameters().get("LraId"); String compensatorLink = req.headers().first(HeaderNames.LINK).orElse(""); - Lra lra = lraPersistentRegistry.get(lraId); + LraImpl lra = lraPersistentRegistry.get(lraId); if (lra == null) { res.status(NOT_FOUND_404).send(); return; @@ -257,14 +290,14 @@ private void join(ServerRequest req, ServerResponse res) { */ private void status(ServerRequest req, ServerResponse res) { String lraId = req.path().pathParameters().get("LraId"); - Lra lra = lraPersistentRegistry.get(lraId); + LraImpl lra = lraPersistentRegistry.get(lraId); if (lra == null) { res.status(NOT_FOUND_404).send(); return; } res.status(OK_200) - .send(lra.status().get().name()); + .send(lra.lraStatus().get().name()); } /** @@ -278,7 +311,7 @@ private void leave(ServerRequest req, ServerResponse res) { String lraId = req.path().pathParameters().get("LraId"); String compensatorLinks = req.content().as(String.class); - Lra lra = lraPersistentRegistry.get(lraId); + LraImpl lra = lraPersistentRegistry.get(lraId); if (lra == null) { res.status(NOT_FOUND_404).send(); } else { @@ -307,13 +340,13 @@ private void recovery(ServerRequest req, ServerResponse res) { }); if (lraUUID.isPresent()) { - Lra lra = lraPersistentRegistry.get(lraUUID.get()); + LraImpl lra = lraPersistentRegistry.get(lraUUID.get()); if (lra != null) { - if (RECOVERABLE_STATUSES.contains(lra.status().get())) { + if (RECOVERABLE_STATUSES.contains(lra.lraStatus().get())) { JsonObject json = JSON.createObjectBuilder() .add("lraId", lra.lraId()) - .add("status", lra.status().get().name()) - .add("recovering", Set.of(LRAStatus.Closed, LRAStatus.Cancelled).contains(lra.status().get())) + .add("status", lra.lraStatus().get().name()) + .add("recovering", Set.of(LRAStatus.Closed, LRAStatus.Cancelled).contains(lra.lraStatus().get())) .build(); res.status(OK_200).send(json); } else { @@ -325,10 +358,10 @@ private void recovery(ServerRequest req, ServerResponse res) { } else { JsonArray jsonValues = lraPersistentRegistry .stream() - .filter(lra -> RECOVERABLE_STATUSES.contains(lra.status().get())) + .filter(lra -> RECOVERABLE_STATUSES.contains(lra.lraStatus().get())) .map(l -> JSON.createObjectBuilder() .add("lraId", l.lraId()) - .add("status", l.status().get().name()) + .add("status", l.lraStatus().get().name()) .build() ) .collect(JSON::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::addAll) @@ -348,7 +381,7 @@ private void get(ServerRequest req, ServerResponse res) { .filter(lra -> lraId.map(id -> lra.lraId().equals(id)).orElse(true)) .map(l -> JSON.createObjectBuilder() .add("lraId", l.lraId()) - .add("status", l.status().get().name()) + .add("status", l.lraStatus().get().name()) .build() ) .collect(JSON::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::addAll) @@ -369,23 +402,23 @@ private void tick(FixedRateInvocation inv) { if (lra.isReadyToDelete()) { lraPersistentRegistry.remove(lra.lraId()); } else { - if (LRAStatus.Cancelling == lra.status().get()) { + if (LRAStatus.Cancelling == lra.lraStatus().get()) { LOGGER.log(Level.DEBUG, "Recovering {0}", lra.lraId()); lra.cancel(); } - if (LRAStatus.Closing == lra.status().get()) { + if (LRAStatus.Closing == lra.lraStatus().get()) { LOGGER.log(Level.DEBUG, "Recovering {0}", lra.lraId()); lra.close(); } - if (lra.checkTimeout() && lra.status().get().equals(LRAStatus.Active)) { + if (lra.checkTimeout() && lra.lraStatus().get().equals(LRAStatus.Active)) { LOGGER.log(Level.DEBUG, "Timeouting {0} ", lra.lraId()); - lra.timeout(); + lra.triggerTimeout(); } - if (Set.of(LRAStatus.Closed, LRAStatus.Cancelled).contains(lra.status().get())) { + if (Set.of(LRAStatus.Closed, LRAStatus.Cancelled).contains(lra.lraStatus().get())) { // If a participant is unable to complete or compensate immediately or because of a failure // then it must remember the fact (by reporting its' status via the @Status method) // until explicitly told that it can clean up using this @Forget annotation. - LOGGER.log(Level.DEBUG, "Forgetting {0} {1}", new Object[] {lra.status().get(), lra.lraId()}); + LOGGER.log(Level.DEBUG, "Forgetting {0} {1}", new Object[] {lra.lraStatus().get(), lra.lraId()}); lra.tryForget(); lra.tryAfter(); } @@ -394,10 +427,6 @@ private void tick(FixedRateInvocation inv) { completedRecovery.getAndSet(new CompletableFuture<>()).complete(null); } - LazyValue getCoordinatorURL() { - return coordinatorURL; - } - private void nextRecoveryCycle() { try { completedRecovery.get().get(1, TimeUnit.SECONDS); @@ -412,25 +441,6 @@ private URI coordinatorUriWithPath(String additionalPath) { return URI.create(coordinatorURL.get().toASCIIString() + "/" + additionalPath); } - /** - * Create a new Lra coordinator. - * - * @return coordinator - */ - public static CoordinatorService create() { - return builder() - .build(); - } - - /** - * Create a new fluent API builder. - * - * @return a new builder - */ - public static Builder builder() { - return new Builder(); - } - /** * Coordinator builder. */ @@ -439,8 +449,8 @@ public static final class Builder implements io.helidon.common.Builder uriSupplier = () -> URI.create(config.get(COORDINATOR_URL_KEY) - .asString() - .orElse(DEFAULT_COORDINATOR_URL)); + .asString() + .orElse(DEFAULT_COORDINATOR_URL)); /** * Configuration needed for configuring coordinator. @@ -453,18 +463,6 @@ public Builder config(Config config) { return this; } - /** - * Custom persistent registry for saving and loading the state of the coordinator. - * Coordinator is not persistent by default. - * - * @param lraPersistentRegistry custom persistent registry - * @return this builder - */ - public Builder persistentRegistry(LraPersistentRegistry lraPersistentRegistry) { - this.lraPersistentRegistry = lraPersistentRegistry; - return this; - } - /** * Supplier for coordinator url. * For supplying url after we know the port of the started server. @@ -487,5 +485,17 @@ public CoordinatorService build() { } return new CoordinatorService(lraPersistentRegistry, uriSupplier, config); } + + /** + * Custom persistent registry for saving and loading the state of the coordinator. + * Coordinator is not persistent by default. + * + * @param lraPersistentRegistry custom persistent registry + * @return this builder + */ + Builder persistentRegistry(LraPersistentRegistry lraPersistentRegistry) { + this.lraPersistentRegistry = lraPersistentRegistry; + return this; + } } } diff --git a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Lra.java b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Lra.java index cd37fc9feb5..5da5101632e 100644 --- a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Lra.java +++ b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Lra.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,30 +15,10 @@ */ package io.helidon.lra.coordinator; -import java.lang.System.Logger.Level; -import java.net.URI; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import io.helidon.common.LazyValue; -import io.helidon.config.Config; -import io.helidon.http.ClientRequestHeaders; import io.helidon.http.HeaderName; import io.helidon.http.HeaderNames; -import io.helidon.metrics.api.Counter; -import io.helidon.metrics.api.MeterRegistry; -import io.helidon.metrics.api.Metrics; -import io.helidon.metrics.api.Timer; import org.eclipse.microprofile.lra.annotation.LRAStatus; @@ -47,298 +27,66 @@ import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_PARENT_CONTEXT_HEADER; import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_RECOVERY_HEADER; -class Lra { - static final HeaderName LRA_HTTP_CONTEXT_HEADER_NAME = HeaderNames.create(LRA_HTTP_CONTEXT_HEADER); - static final HeaderName LRA_HTTP_ENDED_CONTEXT_HEADER_NAME = HeaderNames.create(LRA_HTTP_ENDED_CONTEXT_HEADER); - static final HeaderName LRA_HTTP_PARENT_CONTEXT_HEADER_NAME = HeaderNames.create(LRA_HTTP_PARENT_CONTEXT_HEADER); - static final HeaderName LRA_HTTP_RECOVERY_HEADER_NAME = HeaderNames.create(LRA_HTTP_RECOVERY_HEADER); - - private static final System.Logger LOGGER = System.getLogger(Lra.class.getName()); - - private final LazyValue coordinatorURL; - private long timeout; - private URI parentId; - private final Set compensatorLinks = Collections.synchronizedSet(new HashSet<>()); - - private final String lraId; - private final Config config; - - private final List children = Collections.synchronizedList(new ArrayList<>()); - - private final List participants = new CopyOnWriteArrayList<>(); - - private final AtomicReference status = new AtomicReference<>(LRAStatus.Active); - - private final Lock lock = new ReentrantLock(); - - private boolean isChild; - private long whenReadyToDelete = 0; - - private final MeterRegistry registry = Metrics.globalRegistry(); - private final Counter lraCtr = registry.getOrCreate(Counter.builder("lractr")); - private final Timer lrfLifeSpanTmr = registry.getOrCreate(Timer.builder("lralifespantmr")); - private final Timer.Sample lraLifeSpanTmrSample = Timer.start(registry); - - Lra(CoordinatorService coordinatorService, String lraUUID, Config config) { - lraId = lraUUID; - this.config = config; - lraCtr.increment(); - coordinatorURL = LazyValue.create(coordinatorService.getCoordinatorURL()); - } - - Lra(CoordinatorService coordinatorService, String lraUUID, URI parentId, Config config) { - lraId = lraUUID; - this.parentId = parentId; - this.config = config; - lraCtr.increment(); - coordinatorURL = LazyValue.create(coordinatorService.getCoordinatorURL()); - } - - String lraId() { - return lraId; - } - - String parentId() { - return Optional.ofNullable(parentId).map(URI::toASCIIString).orElse(null); - } - - boolean isChild() { - return isChild; - } - - void setChild(boolean child) { - isChild = child; - } - - long getTimeout() { - return timeout; - } - - void setStatus(LRAStatus status) { - this.status.set(status); - } - - long getWhenReadyToDelete() { - return this.whenReadyToDelete; - } - - void setWhenReadyToDelete(long whenReadyToDelete) { - this.whenReadyToDelete = whenReadyToDelete; - } - - void setTimeout(long timeout) { - this.timeout = timeout; - } - - List getParticipants() { - return this.participants; - } - - void setupTimeout(long timeLimit) { - if (timeLimit != 0) { - this.timeout = System.currentTimeMillis() + timeLimit; - } else { - this.timeout = 0; - } - } - - boolean checkTimeout() { - return timeout > 0 && timeout < System.currentTimeMillis(); - } - - void addParticipant(String compensatorLink) { - if (compensatorLinks.add(compensatorLink)) { - Participant participant = new Participant(config); - participant.parseCompensatorLinks(compensatorLink); - participants.add(participant); - } - } - - void removeParticipant(String compensatorUrl) { - Set forRemove = participants.stream() - .filter(p -> p.equalCompensatorUris(compensatorUrl)) - .collect(Collectors.toSet()); - forRemove.forEach(participants::remove); - } - - void addChild(Lra lra) { - children.add(lra); - lra.isChild = true; - } - - Consumer headers() { - return headers -> { - headers.add(LRA_HTTP_CONTEXT_HEADER_NAME, lraContextId()); - headers.add(LRA_HTTP_ENDED_CONTEXT_HEADER_NAME, lraContextId()); - Optional.ofNullable(parentId) - .map(URI::toASCIIString) - .ifPresent(s -> headers.add(LRA_HTTP_PARENT_CONTEXT_HEADER_NAME, s)); - headers.add(LRA_HTTP_RECOVERY_HEADER_NAME, lraContextId() + "/recovery"); - }; - } - - void close() { - Set allowedStatuses = Set.of(LRAStatus.Active, LRAStatus.Closing); - if (LRAStatus.Closing != status.updateAndGet(old -> allowedStatuses.contains(old) ? LRAStatus.Closing : old)) { - LOGGER.log(Level.WARNING, "Can't close LRA, it's already " + status.get().name() + " " + this.lraId); - return; - } - lraLifeSpanTmrSample.stop(lrfLifeSpanTmr); - if (lock.tryLock()) { - try { - sendComplete(); - // needs to go before nested close, so we know if nested was already closed - // or not(non closed nested can't get @Forget call) - forgetNested(); - for (Lra nestedLra : children) { - nestedLra.close(); - } - trySendAfterLRA(); - markForDeletion(); - } finally { - lock.unlock(); - } - } - } - - void cancel() { - Set allowedStatuses = Set.of(LRAStatus.Active, LRAStatus.Cancelling); - if (LRAStatus.Cancelling != status.updateAndGet(old -> allowedStatuses.contains(old) ? LRAStatus.Cancelling : old) - && !isChild) { // nested can be compensated even if closed - LOGGER.log(Level.WARNING, "Can't cancel LRA, it's already " + status.get().name() + " " + this.lraId); - return; - } - lraLifeSpanTmrSample.stop(lrfLifeSpanTmr); - for (Lra nestedLra : children) { - nestedLra.cancel(); - } - if (lock.tryLock()) { - try { - sendCancel(); - trySendAfterLRA(); - trySendForgetLRA(); - markForDeletion(); - } finally { - lock.unlock(); - } - } - } - - void timeout() { - for (Lra nestedLra : children) { - if (nestedLra.participants.stream().anyMatch(p -> p.state().isFinal() || p.isListenerOnly())) { - nestedLra.timeout(); - } - } - cancel(); - if (lock.tryLock()) { - try { - trySendAfterLRA(); - } finally { - lock.unlock(); - } - } - } - - private boolean forgetNested() { - for (Lra nestedLra : children) { - //dont do forget not yet closed nested lra - if (nestedLra.status.get() != LRAStatus.Closed) continue; - boolean allDone = true; - for (Participant participant : nestedLra.participants) { - if (participant.getForgetURI().isEmpty() || participant.isForgotten()) continue; - allDone = participant.sendForget(nestedLra) && allDone; - } - if (!allDone) return false; - } - return true; - } - - - boolean tryAfter() { - if (lock.tryLock()) { - try { - return trySendAfterLRA(); - } finally { - lock.unlock(); - } - } - return false; - } - - boolean tryForget() { - if (lock.tryLock()) { - try { - return trySendForgetLRA(); - } finally { - lock.unlock(); - } - } - return false; - } - - private boolean trySendForgetLRA() { - boolean allFinished = true; - for (Participant participant : participants) { - if (participant.getForgetURI().isEmpty() || participant.isForgotten()) continue; - if (Set.of( - Participant.Status.FAILED_TO_COMPLETE, - Participant.Status.FAILED_TO_COMPENSATE - ).contains(participant.state())) { - allFinished = participant.sendForget(this) && allFinished; - } - } - return allFinished; - } - - AtomicReference status() { - return status; - } - - String lraContextId() { - return coordinatorURL.get().toASCIIString() + "/" + lraId; - } - - private void sendComplete() { - boolean allClosed = true; - for (Participant participant : participants) { - if (participant.isInEndStateOrListenerOnly() && !isChild) { - continue; - } - allClosed = participant.sendComplete(this) && allClosed; - } - if (allClosed) { - this.status().compareAndSet(LRAStatus.Closing, LRAStatus.Closed); - } - } - - private void sendCancel() { - boolean allDone = true; - for (Participant participant : participants) { - if (participant.isInEndStateOrListenerOnly() && !isChild) { - continue; - } - allDone = participant.sendCancel(this) && allDone; - } - if (allDone) { - this.status().compareAndSet(LRAStatus.Cancelling, LRAStatus.Cancelled); - } - } - - private boolean trySendAfterLRA() { - boolean allSent = true; - for (Participant participant : participants) { - allSent = participant.trySendAfterLRA(this) && allSent; - } - return allSent; - } - - boolean isReadyToDelete() { - return whenReadyToDelete != 0 && whenReadyToDelete < System.currentTimeMillis(); - } - - void markForDeletion() { - // delete after 10 minutes - whenReadyToDelete = (10 * 60 * 1000) + System.currentTimeMillis(); - } +/** + * Long Running Action managed by coordinator. + */ +public interface Lra { + /** + * LRA header name. + */ + HeaderName LRA_HTTP_CONTEXT_HEADER_NAME = HeaderNames.create(LRA_HTTP_CONTEXT_HEADER); + /** + * LRA ended header name. + */ + HeaderName LRA_HTTP_ENDED_CONTEXT_HEADER_NAME = HeaderNames.create(LRA_HTTP_ENDED_CONTEXT_HEADER); + /** + * LRA parent header name. + */ + HeaderName LRA_HTTP_PARENT_CONTEXT_HEADER_NAME = HeaderNames.create(LRA_HTTP_PARENT_CONTEXT_HEADER); + /** + * LRA recovery header name. + */ + HeaderName LRA_HTTP_RECOVERY_HEADER_NAME = HeaderNames.create(LRA_HTTP_RECOVERY_HEADER); + + /** + * ID of the LRA used by this coordinator. + * + * @return lraId without coordinator URI prefix + */ + String lraId(); + + /** + * LRA ID of the parent LRA if this LRA has any. + * + * @return id of parent LRA or null + */ + String parentId(); + + /** + * Returns true if this LRA has parent LRA. + * + * @return true if LRA has parent + */ + boolean isChild(); + + /** + * Returns exact time when will LRA timeout in millis. + * + * @return time of timeout in millis + */ + long timeout(); + + /** + * All participants enrolled in this LRA. + * + * @return list of participants enrolled in this LRA + */ + List participants(); + + /** + * Status of this LRA. + * + * @return status + */ + LRAStatus status(); } diff --git a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/LraDatabasePersistentRegistry.java b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/LraDatabasePersistentRegistry.java index c38ef83539d..b3b270a54cb 100644 --- a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/LraDatabasePersistentRegistry.java +++ b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/LraDatabasePersistentRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +19,9 @@ import java.net.URI; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -35,46 +35,46 @@ class LraDatabasePersistentRegistry implements LraPersistentRegistry { - private final Map lraMap = Collections.synchronizedMap(new HashMap<>()); - private final Config config; - private final DbClient dbClient; private static final Pattern LRA_ID_PATTERN = Pattern.compile(".*/([^/?]+).*"); + private final Map lraMap = Collections.synchronizedMap(new HashMap<>()); + private final Config config; + private final Supplier dbClient; + private final Boolean persistent; LraDatabasePersistentRegistry(Config config) { this.config = config; - this.dbClient = DbClient.builder() - .config(config.get("db")) - .build(); - - DbTransaction tx = dbClient.transaction(); - tx.namedDml("create-lra-table"); - tx.namedDml("create-participant-table"); - tx.commit(); - } - - @Override - public Lra get(String lraId) { - return lraMap.get(lraId); - } - - @Override - public void put(String key, Lra lra) { - lraMap.put(key, lra); - } - - @Override - public void remove(String key) { - lraMap.remove(key); + this.persistent = config.get("persistence").asBoolean().orElse(true); + if (persistent) { + this.dbClient = () -> DbClient.builder() + .config(config.get("db")) + .build(); + DbTransaction tx = dbClient.get().transaction(); + tx.namedDml("create-lra-table"); + tx.namedDml("create-participant-table"); + tx.commit(); + } else { + this.dbClient = () -> { + throw new IllegalStateException("Persistence is not enabled"); + }; + } } - @Override - public Stream stream() { - return lraMap.values().stream(); + static String parseLRAId(String lraUri) { + Matcher m = LRA_ID_PATTERN.matcher(lraUri); + if (!m.matches()) { + //LRA uri format + throw new RuntimeException("Error when parsing lraUri: " + lraUri); + } + return m.group(1); } @Override public void load(CoordinatorService coordinatorService) { - DbExecute tx = dbClient.execute(); + if (!persistent) { + return; + } + + DbExecute tx = dbClient.get().execute(); tx.namedQuery("load").forEach(row -> { String lraId = row.column("ID").get(String.class); String parentId = row.column("PARENT_ID").get(String.class); @@ -96,11 +96,11 @@ public void load(CoordinatorService coordinatorService) { Integer remainingCloseAttempts = row.column("REMAINING_CLOSE_ATTEMPTS").get(Integer.class); Integer remainingAfterAttempts = row.column("REMAINING_AFTER_ATTEMPTS").get(Integer.class); - Lra lra = lraMap.get(lraId); + LraImpl lra = lraMap.get(lraId); if (lra == null) { - lra = new Lra(coordinatorService, lraId, - Optional.ofNullable(parentId).map(URI::create).orElse(null), - config); + lra = new LraImpl(coordinatorService, lraId, + Optional.ofNullable(parentId).map(URI::create).orElse(null), + config); lra.setTimeout(timeout); lra.setStatus(LRAStatus.valueOf(lraStatus)); lra.setChild(isChild); @@ -108,22 +108,21 @@ public void load(CoordinatorService coordinatorService) { } if (participantStatus != null) { - Participant participant = new Participant(config); - participant.setCompleteURI(Optional.ofNullable(completeLink).map(URI::create).orElse(null)); - participant.setCompensateURI(Optional.ofNullable(compensateLink).map(URI::create).orElse(null)); - participant.setAfterURI(Optional.ofNullable(afterLink).map(URI::create).orElse(null)); - participant.setForgetURI(Optional.ofNullable(forgetLink).map(URI::create).orElse(null)); - participant.setStatusURI(Optional.ofNullable(statusLink).map(URI::create).orElse(null)); - participant.setStatus(Participant.Status.valueOf(participantStatus)); - participant.setCompensateStatus(Participant.CompensateStatus.valueOf(compensateStatus)); - participant.setForgetStatus(Participant.ForgetStatus.valueOf(forgetStatus)); - participant.setAfterLraStatus(Participant.AfterLraStatus.valueOf(afterStatus)); - participant.setSendingStatus(Participant.SendingStatus.valueOf(sendingStatus)); - participant.setRemainingCloseAttempts(remainingCloseAttempts); - participant.setRemainingAfterAttempts(remainingAfterAttempts); - - List participants = lra.getParticipants(); - participants.add(participant); + ParticipantImpl participant = new ParticipantImpl(config); + participant.completeURI(Optional.ofNullable(completeLink).map(URI::create).orElse(null)); + participant.compensateURI(Optional.ofNullable(compensateLink).map(URI::create).orElse(null)); + participant.afterURI(Optional.ofNullable(afterLink).map(URI::create).orElse(null)); + participant.forgetURI(Optional.ofNullable(forgetLink).map(URI::create).orElse(null)); + participant.statusURI(Optional.ofNullable(statusLink).map(URI::create).orElse(null)); + participant.status(ParticipantImpl.Status.valueOf(participantStatus)); + participant.compensateStatus(ParticipantImpl.CompensateStatus.valueOf(compensateStatus)); + participant.forgetStatus(ParticipantImpl.ForgetStatus.valueOf(forgetStatus)); + participant.afterLraStatus(ParticipantImpl.AfterLraStatus.valueOf(afterStatus)); + participant.sendingStatus(ParticipantImpl.SendingStatus.valueOf(sendingStatus)); + participant.remainingCloseAttempts(remainingCloseAttempts); + participant.remainingAfterAttempts(remainingAfterAttempts); + + lra.addParticipant(participant); } lraMap.put(lraId, lra); }); @@ -139,44 +138,68 @@ public void load(CoordinatorService coordinatorService) { @Override public void save() { - DbTransaction tx = dbClient.transaction(); + if (!persistent) { + return; + } + + DbTransaction tx = dbClient.get().transaction(); cleanUp(tx); saveAll(tx); tx.commit(); } + @Override + public LraImpl get(String lraId) { + return lraMap.get(lraId); + } + + @Override + public void put(String key, LraImpl lra) { + lraMap.put(key, lra); + } + + @Override + public void remove(String key) { + lraMap.remove(key); + } + + @Override + public Stream stream() { + return lraMap.values().stream(); + } + private void saveAll(DbTransaction tx) { lraMap.values().forEach(lra -> insertLra(tx, lra)); } - private void insertLra(DbTransaction tx, Lra lra) { + private void insertLra(DbTransaction tx, LraImpl lra) { tx.namedInsert("insert-lra", - lra.lraId(), - lra.parentId(), - lra.getTimeout(), - lra.status().get().name(), - lra.isChild(), - lra.getWhenReadyToDelete()); + lra.lraId(), + lra.parentId(), + lra.timeout(), + lra.lraStatus().get().name(), + lra.isChild(), + lra.getWhenReadyToDelete()); // save all participants of the lra - lra.getParticipants().forEach(participant -> insertParticipant(tx, lra, participant)); + lra.participants().forEach(participant -> insertParticipant(tx, lra, (ParticipantImpl) participant)); } - private void insertParticipant(DbTransaction tx, Lra lra, Participant p) { + private void insertParticipant(DbTransaction tx, LraImpl lra, ParticipantImpl p) { tx.namedInsert("insert-participant", - lra.lraId(), - p.state().name(), - p.getCompensateStatus().name(), - p.getForgetStatus().name(), - p.getAfterLraStatus().name(), - p.getSendingStatus().name(), - p.getRemainingCloseAttempts(), - p.getRemainingAfterAttempts(), - p.getCompleteURI().map(URI::toASCIIString).orElse(null), - p.getCompensateURI().map(URI::toASCIIString).orElse(null), - p.getAfterURI().map(URI::toASCIIString).orElse(null), - p.getForgetURI().map(URI::toASCIIString).orElse(null), - p.getStatusURI().map(URI::toASCIIString).orElse(null)); + lra.lraId(), + p.state().name(), + p.compensateStatus().name(), + p.forgetStatus().name(), + p.afterLraStatus().name(), + p.sendingStatus().name(), + p.remainingCloseAttempts(), + p.remainingAfterAttempts(), + p.completeURI().map(URI::toASCIIString).orElse(null), + p.compensateURI().map(URI::toASCIIString).orElse(null), + p.afterURI().map(URI::toASCIIString).orElse(null), + p.forgetURI().map(URI::toASCIIString).orElse(null), + p.statusURI().map(URI::toASCIIString).orElse(null)); } private void cleanUp(DbTransaction tx) { @@ -184,13 +207,4 @@ private void cleanUp(DbTransaction tx) { tx.namedDelete("delete-all-participants"); } - static String parseLRAId(String lraUri) { - Matcher m = LRA_ID_PATTERN.matcher(lraUri); - if (!m.matches()) { - //LRA uri format - throw new RuntimeException("Error when parsing lraUri: " + lraUri); - } - return m.group(1); - } - } diff --git a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/LraImpl.java b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/LraImpl.java new file mode 100644 index 00000000000..abc47abf143 --- /dev/null +++ b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/LraImpl.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.lra.coordinator; + +import java.lang.System.Logger.Level; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import io.helidon.common.LazyValue; +import io.helidon.config.Config; +import io.helidon.http.ClientRequestHeaders; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.Timer; + +import org.eclipse.microprofile.lra.annotation.LRAStatus; + +class LraImpl implements Lra { + + private static final System.Logger LOGGER = System.getLogger(LraImpl.class.getName()); + + private final LazyValue coordinatorURL; + private final Set compensatorLinks = Collections.synchronizedSet(new HashSet<>()); + private final String lraId; + private final Config config; + private final List children = Collections.synchronizedList(new ArrayList<>()); + private final List participants = new CopyOnWriteArrayList<>(); + private final AtomicReference status = new AtomicReference<>(LRAStatus.Active); + private final Lock lock = new ReentrantLock(); + private final MeterRegistry registry = Metrics.globalRegistry(); + private final Counter lraCtr = registry.getOrCreate(Counter.builder("lractr")); + private final Timer lrfLifeSpanTmr = registry.getOrCreate(Timer.builder("lralifespantmr")); + private final Timer.Sample lraLifeSpanTmrSample = Timer.start(registry); + private long timeout; + private URI parentId; + private boolean isChild; + private long whenReadyToDelete = 0; + + LraImpl(CoordinatorService coordinatorService, String lraUUID, Config config) { + lraId = lraUUID; + this.config = config; + lraCtr.increment(); + coordinatorURL = LazyValue.create(coordinatorService.coordinatorURL()); + } + + LraImpl(CoordinatorService coordinatorService, String lraUUID, URI parentId, Config config) { + lraId = lraUUID; + this.parentId = parentId; + this.config = config; + lraCtr.increment(); + coordinatorURL = LazyValue.create(coordinatorService.coordinatorURL()); + } + + @Override + public String lraId() { + return lraId; + } + + @Override + public String parentId() { + return Optional.ofNullable(parentId).map(URI::toASCIIString).orElse(null); + } + + @Override + public boolean isChild() { + return isChild; + } + + void setChild(boolean child) { + isChild = child; + } + + @Override + public long timeout() { + return timeout; + } + + void setTimeout(long timeout) { + this.timeout = timeout; + } + + @Override + public List participants() { + return this.participants.stream().map(Participant.class::cast).toList(); + } + + @Override + public LRAStatus status() { + return lraStatus().get(); + } + + void setStatus(LRAStatus status) { + this.status.set(status); + } + + long getWhenReadyToDelete() { + return this.whenReadyToDelete; + } + + void setWhenReadyToDelete(long whenReadyToDelete) { + this.whenReadyToDelete = whenReadyToDelete; + } + + void addParticipant(ParticipantImpl participant) { + this.participants.add(participant); + } + + void setupTimeout(long timeLimit) { + if (timeLimit != 0) { + this.timeout = System.currentTimeMillis() + timeLimit; + } else { + this.timeout = 0; + } + } + + boolean checkTimeout() { + return timeout > 0 && timeout < System.currentTimeMillis(); + } + + void addParticipant(String compensatorLink) { + if (compensatorLinks.add(compensatorLink)) { + ParticipantImpl participant = new ParticipantImpl(config); + participant.parseCompensatorLinks(compensatorLink); + participants.add(participant); + } + } + + void removeParticipant(String compensatorUrl) { + Set forRemove = participants.stream() + .filter(p -> p.equalCompensatorUris(compensatorUrl)) + .collect(Collectors.toSet()); + forRemove.forEach(participants::remove); + } + + void addChild(LraImpl lra) { + children.add(lra); + lra.isChild = true; + } + + Consumer headers() { + return headers -> { + headers.add(LRA_HTTP_CONTEXT_HEADER_NAME, lraContextId()); + headers.add(LRA_HTTP_ENDED_CONTEXT_HEADER_NAME, lraContextId()); + Optional.ofNullable(parentId) + .map(URI::toASCIIString) + .ifPresent(s -> headers.add(LRA_HTTP_PARENT_CONTEXT_HEADER_NAME, s)); + headers.add(LRA_HTTP_RECOVERY_HEADER_NAME, lraContextId() + "/recovery"); + }; + } + + void close() { + Set allowedStatuses = Set.of(LRAStatus.Active, LRAStatus.Closing); + if (LRAStatus.Closing != status.updateAndGet(old -> allowedStatuses.contains(old) ? LRAStatus.Closing : old)) { + LOGGER.log(Level.WARNING, "Can't close LRA, it's already " + status.get().name() + " " + this.lraId); + return; + } + lraLifeSpanTmrSample.stop(lrfLifeSpanTmr); + if (lock.tryLock()) { + try { + sendComplete(); + // needs to go before nested close, so we know if nested was already closed + // or not(non closed nested can't get @Forget call) + forgetNested(); + for (LraImpl nestedLra : children) { + nestedLra.close(); + } + trySendAfterLRA(); + markForDeletion(); + } finally { + lock.unlock(); + } + } + } + + void cancel() { + Set allowedStatuses = Set.of(LRAStatus.Active, LRAStatus.Cancelling); + if (LRAStatus.Cancelling != status.updateAndGet(old -> allowedStatuses.contains(old) ? LRAStatus.Cancelling : old) + && !isChild) { // nested can be compensated even if closed + LOGGER.log(Level.WARNING, "Can't cancel LRA, it's already " + status.get().name() + " " + this.lraId); + return; + } + lraLifeSpanTmrSample.stop(lrfLifeSpanTmr); + for (LraImpl nestedLra : children) { + nestedLra.cancel(); + } + if (lock.tryLock()) { + try { + sendCancel(); + trySendAfterLRA(); + trySendForgetLRA(); + markForDeletion(); + } finally { + lock.unlock(); + } + } + } + + void triggerTimeout() { + for (LraImpl nestedLra : children) { + if (nestedLra.participants.stream().anyMatch(p -> p.state().isFinal() || p.isListenerOnly())) { + nestedLra.triggerTimeout(); + } + } + cancel(); + if (lock.tryLock()) { + try { + trySendAfterLRA(); + } finally { + lock.unlock(); + } + } + } + + boolean tryAfter() { + if (lock.tryLock()) { + try { + return trySendAfterLRA(); + } finally { + lock.unlock(); + } + } + return false; + } + + boolean tryForget() { + if (lock.tryLock()) { + try { + return trySendForgetLRA(); + } finally { + lock.unlock(); + } + } + return false; + } + + AtomicReference lraStatus() { + return status; + } + + String lraContextId() { + return coordinatorURL.get().toASCIIString() + "/" + lraId; + } + + boolean isReadyToDelete() { + return whenReadyToDelete != 0 && whenReadyToDelete < System.currentTimeMillis(); + } + + void markForDeletion() { + // delete after 10 minutes + whenReadyToDelete = (10 * 60 * 1000) + System.currentTimeMillis(); + } + + private boolean forgetNested() { + for (LraImpl nestedLra : children) { + //don't do forget not yet closed nested lra + if (nestedLra.status.get() != LRAStatus.Closed) { + continue; + } + boolean allDone = true; + for (ParticipantImpl participant : nestedLra.participants) { + if (participant.forgetURI().isEmpty() || participant.isForgotten()) { + continue; + } + allDone = participant.sendForget(nestedLra) && allDone; + } + if (!allDone) { + return false; + } + } + return true; + } + + private boolean trySendForgetLRA() { + boolean allFinished = true; + for (ParticipantImpl participant : participants) { + if (participant.forgetURI().isEmpty() || participant.isForgotten()) { + continue; + } + if (Set.of( + ParticipantImpl.Status.FAILED_TO_COMPLETE, + ParticipantImpl.Status.FAILED_TO_COMPENSATE + ).contains(participant.state())) { + allFinished = participant.sendForget(this) && allFinished; + } + } + return allFinished; + } + + private void sendComplete() { + boolean allClosed = true; + for (ParticipantImpl participant : participants) { + if (participant.isInEndStateOrListenerOnly() && !isChild) { + continue; + } + allClosed = participant.sendComplete(this) && allClosed; + } + if (allClosed) { + this.lraStatus().compareAndSet(LRAStatus.Closing, LRAStatus.Closed); + } + } + + private void sendCancel() { + boolean allDone = true; + for (ParticipantImpl participant : participants) { + if (participant.isInEndStateOrListenerOnly() && !isChild) { + continue; + } + allDone = participant.sendCancel(this) && allDone; + } + if (allDone) { + this.lraStatus().compareAndSet(LRAStatus.Cancelling, LRAStatus.Cancelled); + } + } + + private boolean trySendAfterLRA() { + boolean allSent = true; + for (ParticipantImpl participant : participants) { + allSent = participant.trySendAfterLRA(this) && allSent; + } + return allSent; + } +} diff --git a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/LraPersistentRegistry.java b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/LraPersistentRegistry.java index d0aae2c8a2e..1d765a5f561 100644 --- a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/LraPersistentRegistry.java +++ b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/LraPersistentRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ interface LraPersistentRegistry { * @param lraId to look for * @return lra if exist */ - Lra get(String lraId); + LraImpl get(String lraId); /** * Add new Lra. @@ -47,7 +47,7 @@ interface LraPersistentRegistry { * @param lraId id of new lra * @param lra Lra */ - void put(String lraId, Lra lra); + void put(String lraId, LraImpl lra); /** * Remove lra by id. @@ -61,6 +61,6 @@ interface LraPersistentRegistry { * * @return stream of all the Lras */ - Stream stream(); + Stream stream(); } diff --git a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Participant.java b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Participant.java index 8ce4f5a3309..2514923fc17 100644 --- a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Participant.java +++ b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/Participant.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,577 +13,49 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.helidon.lra.coordinator; -import java.lang.System.Logger.Level; import java.net.URI; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; import java.util.Optional; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import io.helidon.config.Config; -import io.helidon.webclient.api.HttpClientResponse; -import io.helidon.webclient.api.WebClient; - -import org.eclipse.microprofile.lra.annotation.LRAStatus; -import org.eclipse.microprofile.lra.annotation.ParticipantStatus; - -import static io.helidon.lra.coordinator.Lra.LRA_HTTP_CONTEXT_HEADER_NAME; -import static io.helidon.lra.coordinator.Lra.LRA_HTTP_ENDED_CONTEXT_HEADER_NAME; -import static io.helidon.lra.coordinator.Lra.LRA_HTTP_RECOVERY_HEADER_NAME; -import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.Active; -import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.Compensated; -import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.Compensating; -import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.Completed; -import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.Completing; -import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.FailedToCompensate; -import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.FailedToComplete; -import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.valueOf; - -class Participant { - - private static final int RETRY_CNT = 60; - private static final int SYNCHRONOUS_RETRY_CNT = 5; - - private static final System.Logger LOGGER = System.getLogger(Participant.class.getName()); - private final AtomicReference compensateCalled = new AtomicReference<>(CompensateStatus.NOT_SENT); - private final AtomicReference forgetCalled = new AtomicReference<>(ForgetStatus.NOT_SENT); - private final AtomicReference afterLRACalled = new AtomicReference<>(AfterLraStatus.NOT_SENT); - private final AtomicReference sendingStatus = new AtomicReference<>(SendingStatus.NOT_SENDING); - private final AtomicInteger remainingCloseAttempts = new AtomicInteger(RETRY_CNT); - private final AtomicInteger remainingAfterLraAttempts = new AtomicInteger(RETRY_CNT); - - private final AtomicReference status = new AtomicReference<>(Status.ACTIVE); - private final Map compensatorLinks = new HashMap<>(); - private final long timeout; - private final WebClient webClient = WebClient.builder().build(); - - enum Status { - ACTIVE(Active, null, null, false, Set.of(Completing, Compensating)), - - COMPENSATED(Compensated, null, null, true, Set.of(Compensated)), - COMPLETED(Completed, null, null, true, Set.of(Completed)), - FAILED_TO_COMPENSATE(FailedToCompensate, null, null, true, Set.of()), - FAILED_TO_COMPLETE(FailedToComplete, null, null, true, Set.of()), - - CLIENT_COMPENSATING(Compensating, COMPENSATED, FAILED_TO_COMPENSATE, false, - Set.of(Active, Compensated, FailedToCompensate)), - CLIENT_COMPLETING(Completing, COMPLETED, FAILED_TO_COMPLETE, false, Set.of(Active, Completed, FailedToComplete)), - COMPENSATING(Compensating, COMPENSATED, FAILED_TO_COMPENSATE, false, Set.of(Active, Compensated, FailedToCompensate)), - COMPLETING(Completing, COMPLETED, FAILED_TO_COMPLETE, false, Set.of(Active, Completed, FailedToComplete)); - - private final ParticipantStatus participantStatus; - private final Status successFinalStatus; - private final Status failedFinalStatus; - private final boolean finalState; - private final Set validNextStates; - - Status(ParticipantStatus participantStatus, - Status successFinalStatus, - Status failedFinalStatus, - boolean finalState, - Set validNextStates) { - this.participantStatus = participantStatus; - this.successFinalStatus = successFinalStatus; - this.failedFinalStatus = failedFinalStatus; - this.finalState = finalState; - this.validNextStates = validNextStates; - } - - ParticipantStatus participantStatus() { - return participantStatus; - } - - boolean isFinal() { - return finalState; - } - - boolean validateNextStatus(ParticipantStatus participantStatus) { - return validNextStates.contains(participantStatus); - } - - Optional successFinalStatus() { - return Optional.ofNullable(successFinalStatus.participantStatus()); - } - - Optional failedFinalStatus() { - return Optional.ofNullable(failedFinalStatus.participantStatus); - } - } - enum SendingStatus { - SENDING, NOT_SENDING; - } - - enum AfterLraStatus { - NOT_SENT, SENDING, SENT; - } - - enum ForgetStatus { - NOT_SENT, SENDING, SENT; - } - - enum CompensateStatus { - NOT_SENT, SENDING, SENT; - } - - Participant(Config config) { - timeout = config.get("timeout") - .asLong() - .orElse(500L); - } - - void parseCompensatorLinks(String compensatorLinks) { - Stream.of(compensatorLinks.split(",")) - .filter(s -> !s.isBlank()) - .map(Link::valueOf) - .forEach(link -> this.compensatorLinks.put(link.rel(), link.uri())); - } - - Optional getCompensatorLink(String rel) { - return Optional.ofNullable(compensatorLinks.get(rel)); - } +/** + * LRA participant managed by coordinator. + */ +public interface Participant { /** * Invoked when closed 200, 202, 409, 410. + * + * @return optional uri */ - Optional getCompleteURI() { - return getCompensatorLink("complete"); - } - - void setCompleteURI(URI completeURI) { - compensatorLinks.put("complete", completeURI); - } + Optional completeURI(); /** * Invoked when cancelled 200, 202, 409, 410. + * + * @return optional uri */ - Optional getCompensateURI() { - return getCompensatorLink("compensate"); - } - - void setCompensateURI(URI compensateURI) { - compensatorLinks.put("compensate", compensateURI); - } + Optional compensateURI(); /** * Invoked when finalized 200. + * + * @return optional uri */ - Optional getAfterURI() { - return getCompensatorLink("after"); - } - - void setAfterURI(URI afterURI) { - compensatorLinks.put("after", afterURI); - } + Optional afterURI(); /** * Invoked when cleaning up 200, 410. + * + * @return optional uri */ - Optional getForgetURI() { - return getCompensatorLink("forget"); - } - - void setForgetURI(URI forgetURI) { - compensatorLinks.put("forget", forgetURI); - } + Optional forgetURI(); /** * Directly updates status of participant 200, 202, 410. + * + * @return optional uri */ - Optional getStatusURI() { - return getCompensatorLink("status"); - } - - void setStatusURI(URI statusURI) { - compensatorLinks.put("status", statusURI); - } - - CompensateStatus getCompensateStatus() { - return this.compensateCalled.get(); - } - - void setCompensateStatus(CompensateStatus compensateStatus) { - this.compensateCalled.set(compensateStatus); - } - - void setStatus(Status status) { - this.status.set(status); - } - - ForgetStatus getForgetStatus() { - return this.forgetCalled.get(); - } - - void setForgetStatus(ForgetStatus forgetStatus) { - this.forgetCalled.set(forgetStatus); - } - - AfterLraStatus getAfterLraStatus() { - return this.afterLRACalled.get(); - } - - void setAfterLraStatus(AfterLraStatus afterLraStatus) { - this.afterLRACalled.set(afterLraStatus); - } - - SendingStatus getSendingStatus() { - return this.sendingStatus.get(); - } - - void setSendingStatus(SendingStatus sendingStatus) { - this.sendingStatus.set(sendingStatus); - } - - int getRemainingCloseAttempts() { - return this.remainingCloseAttempts.get(); - } - - void setRemainingCloseAttempts(int remainingCloseAttempts) { - this.remainingCloseAttempts.set(remainingCloseAttempts); - } - - int getRemainingAfterAttempts() { - return this.remainingAfterLraAttempts.get(); - } - - void setRemainingAfterAttempts(int remainingAfterAttempts) { - this.remainingAfterLraAttempts.set(remainingAfterAttempts); - } - - Status state() { - return status.get(); - } - - boolean isForgotten() { - return forgetCalled.get() == ForgetStatus.SENT; - } - - boolean isListenerOnly() { - return getCompleteURI().isEmpty() && getCompensateURI().isEmpty(); - } - - boolean isInEndStateOrListenerOnly() { - return isListenerOnly() || status.get().isFinal(); - } - - boolean sendCancel(Lra lra) { - Optional endpointURI = getCompensateURI(); - for (AtomicInteger i = new AtomicInteger(0); i.getAndIncrement() < SYNCHRONOUS_RETRY_CNT;) { - if (!sendingStatus.compareAndSet(SendingStatus.NOT_SENDING, SendingStatus.SENDING)) return false; - if (!compensateCalled.compareAndSet(CompensateStatus.NOT_SENT, CompensateStatus.SENDING)) return false; - LOGGER.log(Level.DEBUG, () -> "Sending compensate, sync retry: " + i.get() - + ", status: " + status.get().name() - + " statusUri: " + getStatusURI().map(URI::toASCIIString).orElse(null)); - HttpClientResponse response = null; - try { - // call for client status only on retries and when status uri is known - if (!status.get().equals(Status.ACTIVE) && getStatusURI().isPresent()) { - // If the participant does not support idempotency then it MUST be able to report its status - // by annotating one of the methods with the @Status annotation which should report the status - // in case we can't retrieve status from participant just retry n times - ParticipantStatus reportedClientStatus = retrieveStatus(lra, Compensating).orElse(null); - if (reportedClientStatus == Compensated) { - LOGGER.log(Level.INFO, "Participant reports it is compensated."); - status.set(Status.COMPENSATED); - return true; - } else if (reportedClientStatus == FailedToCompensate) { - LOGGER.log(Level.INFO, "Participant reports it failed to compensate."); - status.set(Status.FAILED_TO_COMPENSATE); - return true; - } else if (reportedClientStatus == Active) { - // last call didn't reach participant, try call again - } else if (reportedClientStatus == Completed && lra.isChild()) { - // completed participant can be compensated again in case of nested tx - } else if (reportedClientStatus == Compensating) { - LOGGER.log(Level.INFO, "Participant reports it is still compensating."); - status.set(Status.CLIENT_COMPENSATING); - return false; - } else if (remainingCloseAttempts.decrementAndGet() <= 0) { - LOGGER.log(Level.INFO, "Participant didnt report final status after {0} status call retries.", - new Object[] {RETRY_CNT}); - status.set(Status.FAILED_TO_COMPENSATE); - return true; - } else { - // Unknown status, lets try in next recovery cycle - LOGGER.log(Level.INFO, "Unknown status of " + lra.lraId()); - return false; - } - } - - response = webClient.put() - .uri(endpointURI.get()) - .headers(lra.headers()) - .submit(LRAStatus.Cancelled.name()); - - // When timeout occur we loose track of the participant status - // next retry will attempt to retrieve participant status if status uri is available - - switch (response.status().code()) { - // complete or compensated - case 200: - case 410: - LOGGER.log(Level.INFO, "Compensated participant of LRA {0} {1}", - new Object[] {lra.lraId(), this.getCompensateURI()}); - status.set(Status.COMPENSATED); - compensateCalled.set(CompensateStatus.SENT); - return true; - - // retryable - case 202: - // Still compensating, check with @Status later - this.status.set(Status.CLIENT_COMPENSATING); - return false; - case 409: - case 404: - case 503: - default: - throw new Exception(response.status().code() + " " + response.status().reasonPhrase()); - } - - } catch (Exception e) { - LOGGER.log(Level.WARNING, - () -> "Can't reach participant's compensate endpoint: " - + endpointURI.map(URI::toASCIIString).orElse("unknown"), e); - if (remainingCloseAttempts.decrementAndGet() <= 0) { - LOGGER.log(Level.WARNING, "Failed to compensate participant of LRA {0} {1} {2}", - new Object[] {lra.lraId(), this.getCompensateURI(), e.getMessage()}); - status.set(Status.FAILED_TO_COMPENSATE); - } else { - status.set(Status.COMPENSATING); - } - - } finally { - Optional.ofNullable(response).ifPresent(HttpClientResponse::close); - sendingStatus.set(SendingStatus.NOT_SENDING); - compensateCalled.compareAndSet(CompensateStatus.SENDING, CompensateStatus.NOT_SENT); - } - } - return false; - } - - boolean sendComplete(Lra lra) { - Optional endpointURI = getCompleteURI(); - for (AtomicInteger i = new AtomicInteger(0); i.getAndIncrement() < SYNCHRONOUS_RETRY_CNT;) { - if (!sendingStatus.compareAndSet(SendingStatus.NOT_SENDING, SendingStatus.SENDING)) return false; - LOGGER.log(Level.DEBUG, () -> "Sending complete, sync retry: " + i.get() - + ", status: " + status.get().name() - + " statusUri: " + getStatusURI().map(URI::toASCIIString).orElse(null)); - HttpClientResponse response = null; - try { - if (status.get().isFinal()) { - return true; - // call for client status only on retries and when status uri is known - } else if (!status.get().equals(Status.ACTIVE) && getStatusURI().isPresent()) { - // If the participant does not support idempotency then it MUST be able to report its status - // by annotating one of the methods with the @Status annotation which should report the status - // in case we can't retrieve status from participant just retry n times - ParticipantStatus reportedClientStatus = retrieveStatus(lra, Completing).orElse(null); - if (reportedClientStatus == Completed) { - LOGGER.log(Level.INFO, "Participant reports it is completed."); - status.set(Status.COMPLETED); - return true; - } else if (reportedClientStatus == FailedToComplete) { - LOGGER.log(Level.INFO, "Participant reports it failed to complete."); - status.set(Status.FAILED_TO_COMPLETE); - return true; - } else if (reportedClientStatus == Active) { - // last call didn't reach participant, try call again - } else if (reportedClientStatus == Completing) { - LOGGER.log(Level.INFO, "Participant reports it is still completing."); - status.set(Status.CLIENT_COMPLETING); - return false; - } else if (remainingCloseAttempts.decrementAndGet() <= 0) { - LOGGER.log(Level.INFO, "Participant didnt report final status after {0} status call retries.", - new Object[] {RETRY_CNT}); - status.set(Status.FAILED_TO_COMPLETE); - return true; - } else { - // Unknown status, lets try in next recovery cycle - return false; - } - } - response = webClient.put() - .uri(endpointURI.get()) - .headers(lra.headers()) - .submit(LRAStatus.Closed.name()); - // When timeout occur we loose track of the participant status - // next retry will attempt to retrieve participant status if status uri is available - - switch (response.status().code()) { - // complete or compensated - case 200: - case 410: - status.set(Status.COMPLETED); - return true; - - // retryable - case 202: - // Still completing, check with @Status later - this.status.set(Status.CLIENT_COMPLETING); - return false; - case 409: - case 404: - case 503: - default: - throw new Exception(response.status().code() + " " + response.status().reasonPhrase()); - } - - } catch (Exception e) { - LOGGER.log(Level.WARNING, - () -> "Can't reach participant's complete endpoint: " + endpointURI.map(URI::toASCIIString) - .orElse("unknown"), e); - if (remainingCloseAttempts.decrementAndGet() <= 0) { - LOGGER.log(Level.WARNING, "Failed to complete participant of LRA {0} {1} {2}", - new Object[]{lra.lraId(), - this.getCompleteURI(), e.getMessage()}); - status.set(Status.FAILED_TO_COMPLETE); - } else { - status.set(Status.COMPLETING); - } - } finally { - Optional.ofNullable(response).ifPresent(HttpClientResponse::close); - sendingStatus.set(SendingStatus.NOT_SENDING); - } - } - return false; - } - - boolean trySendAfterLRA(Lra lra) { - for (int i = 0; i < SYNCHRONOUS_RETRY_CNT; i++) { - // Participant in right state - if (!isInEndStateOrListenerOnly()) return false; - // LRA in right state - if (!(Set.of(LRAStatus.Closed, LRAStatus.Cancelled).contains(lra.status().get()))) return false; - - HttpClientResponse response = null; - try { - Optional afterURI = getAfterURI(); - if (afterURI.isPresent() && afterLRACalled.compareAndSet(AfterLraStatus.NOT_SENT, AfterLraStatus.SENDING)) { - response = webClient.put() - .uri(afterURI.get()) - .headers(lra.headers()) - .submit(lra.status().get().name()); - - if (response.status().code() == 200) { - afterLRACalled.set(AfterLraStatus.SENT); - } else if (remainingAfterLraAttempts.decrementAndGet() <= 0) { - afterLRACalled.set(AfterLraStatus.SENT); - } - } - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Error when sending after lra", e); - if (remainingAfterLraAttempts.decrementAndGet() <= 0) { - afterLRACalled.set(AfterLraStatus.SENT); - } else { - afterLRACalled.set(AfterLraStatus.NOT_SENT); - } - } finally { - Optional.ofNullable(response).ifPresent(HttpClientResponse::close); - } - if (afterLRACalled.get() == AfterLraStatus.SENT) return true; - } - return false; - } - - - Optional retrieveStatus(Lra lra, ParticipantStatus inProgressStatus) { - URI statusURI = this.getStatusURI().get(); - try (HttpClientResponse response = webClient.get() - .uri(statusURI) - .headers(h -> { - // Dont send parent! - h.add(LRA_HTTP_CONTEXT_HEADER_NAME, lra.lraContextId()); - h.add(LRA_HTTP_RECOVERY_HEADER_NAME, lra.lraContextId() + "/recovery"); - h.add(LRA_HTTP_ENDED_CONTEXT_HEADER_NAME, lra.lraContextId()); - }) - .request()) { - - int code = response.status().code(); - switch (code) { - case 202: - return Optional.of(inProgressStatus); - case 410: //GONE - //Completing -> FailedToComplete ... - return status.get().failedFinalStatus(); - case 503: - case 500: - throw new IllegalStateException(String.format("Client reports unexpected status %s %s, " - + "current participant state is %s, " - + "lra: %s " - + "status uri: %s", - code, - response.as(String.class), - status.get(), - lra.lraId(), - statusURI.toASCIIString())); - default: - ParticipantStatus reportedStatus = valueOf(response.as(String.class)); - Status currentStatus = status.get(); - if (currentStatus.validateNextStatus(reportedStatus)) { - return Optional.of(reportedStatus); - } else { - LOGGER.log(Level.WARNING, - "Client reports unexpected status {0} {1}, " - + "current participant state is {2}, " - + "lra: {3} " - + "status uri: {4}", - new Object[] {code, reportedStatus, currentStatus, lra.lraId(), statusURI.toASCIIString()}); - return Optional.empty(); - } - } - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Error when getting participant status. " + statusURI, e); - // skip dependent compensation call, another retry with status call might be luckier - throw e; - } - } - - boolean sendForget(Lra lra) { - if (!forgetCalled.compareAndSet(ForgetStatus.NOT_SENT, ForgetStatus.SENDING)) return false; - try ( - HttpClientResponse response = webClient.delete() - .uri(getForgetURI().get()) - .headers(lra.headers()) - .request()) { - - int responseStatus = response.status().code(); - if (responseStatus == 200 || responseStatus == 410) { - forgetCalled.set(ForgetStatus.SENT); - } else { - throw new Exception("Unexpected response from participant " + response.status().code()); - } - } catch (Throwable e) { - LOGGER.log(Level.WARNING, "Unable to send forget of lra {0} to {1}", - new Object[] {lra.lraId(), getForgetURI().get()}); - forgetCalled.set(ForgetStatus.NOT_SENT); - } - return forgetCalled.get() == ForgetStatus.SENT; - } - - boolean equalCompensatorUris(String compensatorUris) { - Set links = Arrays.stream(compensatorUris.split(",")) - .map(Link::valueOf) - .collect(Collectors.toSet()); - - for (Link link : links) { - Optional participantsLink = getCompensatorLink(link.rel()); - if (participantsLink.isEmpty()) { - continue; - } - - if (Objects.equals(participantsLink.get(), link.uri())) { - return true; - } - } - - return false; - } + Optional statusURI(); } diff --git a/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/ParticipantImpl.java b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/ParticipantImpl.java new file mode 100644 index 00000000000..abc405007a5 --- /dev/null +++ b/lra/coordinator/server/src/main/java/io/helidon/lra/coordinator/ParticipantImpl.java @@ -0,0 +1,592 @@ +/* + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.lra.coordinator; + +import java.lang.System.Logger.Level; +import java.net.URI; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.config.Config; +import io.helidon.webclient.api.HttpClientResponse; +import io.helidon.webclient.api.WebClient; + +import org.eclipse.microprofile.lra.annotation.LRAStatus; +import org.eclipse.microprofile.lra.annotation.ParticipantStatus; + +import static io.helidon.lra.coordinator.LraImpl.LRA_HTTP_CONTEXT_HEADER_NAME; +import static io.helidon.lra.coordinator.LraImpl.LRA_HTTP_ENDED_CONTEXT_HEADER_NAME; +import static io.helidon.lra.coordinator.LraImpl.LRA_HTTP_RECOVERY_HEADER_NAME; +import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.Active; +import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.Compensated; +import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.Compensating; +import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.Completed; +import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.Completing; +import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.FailedToCompensate; +import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.FailedToComplete; +import static org.eclipse.microprofile.lra.annotation.ParticipantStatus.valueOf; + +class ParticipantImpl implements Participant { + + private static final int RETRY_CNT = 60; + private static final int SYNCHRONOUS_RETRY_CNT = 5; + + private static final System.Logger LOGGER = System.getLogger(Participant.class.getName()); + private final AtomicReference compensateCalled = new AtomicReference<>(CompensateStatus.NOT_SENT); + private final AtomicReference forgetCalled = new AtomicReference<>(ForgetStatus.NOT_SENT); + private final AtomicReference afterLRACalled = new AtomicReference<>(AfterLraStatus.NOT_SENT); + private final AtomicReference sendingStatus = new AtomicReference<>(SendingStatus.NOT_SENDING); + private final AtomicInteger remainingCloseAttempts = new AtomicInteger(RETRY_CNT); + private final AtomicInteger remainingAfterLraAttempts = new AtomicInteger(RETRY_CNT); + + private final AtomicReference status = new AtomicReference<>(Status.ACTIVE); + private final Map compensatorLinks = new HashMap<>(); + private final long timeout; + private final WebClient webClient = WebClient.builder().build(); + + ParticipantImpl(Config config) { + timeout = config.get("timeout") + .asLong() + .orElse(500L); + } + + @Override + public Optional completeURI() { + return compensatorLink("complete"); + } + + @Override + public Optional compensateURI() { + return compensatorLink("compensate"); + } + + @Override + public Optional afterURI() { + return compensatorLink("after"); + } + + @Override + public Optional forgetURI() { + return compensatorLink("forget"); + } + + @Override + public Optional statusURI() { + return compensatorLink("status"); + } + + void parseCompensatorLinks(String compensatorLinks) { + Stream.of(compensatorLinks.split(",")) + .filter(s -> !s.isBlank()) + .map(Link::valueOf) + .forEach(link -> this.compensatorLinks.put(link.rel(), link.uri())); + } + + Optional compensatorLink(String rel) { + return Optional.ofNullable(compensatorLinks.get(rel)); + } + + void completeURI(URI completeURI) { + compensatorLinks.put("complete", completeURI); + } + + void compensateURI(URI compensateURI) { + compensatorLinks.put("compensate", compensateURI); + } + + void afterURI(URI afterURI) { + compensatorLinks.put("after", afterURI); + } + + void forgetURI(URI forgetURI) { + compensatorLinks.put("forget", forgetURI); + } + + void statusURI(URI statusURI) { + compensatorLinks.put("status", statusURI); + } + + CompensateStatus compensateStatus() { + return this.compensateCalled.get(); + } + + void compensateStatus(CompensateStatus compensateStatus) { + this.compensateCalled.set(compensateStatus); + } + + void status(Status status) { + this.status.set(status); + } + + ForgetStatus forgetStatus() { + return this.forgetCalled.get(); + } + + void forgetStatus(ForgetStatus forgetStatus) { + this.forgetCalled.set(forgetStatus); + } + + AfterLraStatus afterLraStatus() { + return this.afterLRACalled.get(); + } + + void afterLraStatus(AfterLraStatus afterLraStatus) { + this.afterLRACalled.set(afterLraStatus); + } + + SendingStatus sendingStatus() { + return this.sendingStatus.get(); + } + + void sendingStatus(SendingStatus sendingStatus) { + this.sendingStatus.set(sendingStatus); + } + + int remainingCloseAttempts() { + return this.remainingCloseAttempts.get(); + } + + void remainingCloseAttempts(int remainingCloseAttempts) { + this.remainingCloseAttempts.set(remainingCloseAttempts); + } + + int remainingAfterAttempts() { + return this.remainingAfterLraAttempts.get(); + } + + void remainingAfterAttempts(int remainingAfterAttempts) { + this.remainingAfterLraAttempts.set(remainingAfterAttempts); + } + + Status state() { + return status.get(); + } + + boolean isForgotten() { + return forgetCalled.get() == ForgetStatus.SENT; + } + + boolean isListenerOnly() { + return completeURI().isEmpty() && compensateURI().isEmpty(); + } + + boolean isInEndStateOrListenerOnly() { + return isListenerOnly() || status.get().isFinal(); + } + + boolean sendCancel(LraImpl lra) { + Optional endpointURI = compensateURI(); + for (AtomicInteger i = new AtomicInteger(0); i.getAndIncrement() < SYNCHRONOUS_RETRY_CNT;) { + if (!sendingStatus.compareAndSet(SendingStatus.NOT_SENDING, SendingStatus.SENDING)) { + return false; + } + if (!compensateCalled.compareAndSet(CompensateStatus.NOT_SENT, CompensateStatus.SENDING)) { + return false; + } + LOGGER.log(Level.DEBUG, () -> "Sending compensate, sync retry: " + i.get() + + ", status: " + status.get().name() + + " statusUri: " + statusURI().map(URI::toASCIIString).orElse(null)); + HttpClientResponse response = null; + try { + // call for client status only on retries and when status uri is known + if (!status.get().equals(Status.ACTIVE) && statusURI().isPresent()) { + // If the participant does not support idempotency then it MUST be able to report its status + // by annotating one of the methods with the @Status annotation which should report the status + // in case we can't retrieve status from participant just retry n times + ParticipantStatus reportedClientStatus = retrieveStatus(lra, Compensating).orElse(null); + if (reportedClientStatus == Compensated) { + LOGGER.log(Level.INFO, "Participant reports it is compensated."); + status.set(Status.COMPENSATED); + return true; + } else if (reportedClientStatus == FailedToCompensate) { + LOGGER.log(Level.INFO, "Participant reports it failed to compensate."); + status.set(Status.FAILED_TO_COMPENSATE); + return true; + } else if (reportedClientStatus == Active) { + // last call didn't reach participant, try call again + } else if (reportedClientStatus == Completed && lra.isChild()) { + // completed participant can be compensated again in case of nested tx + } else if (reportedClientStatus == Compensating) { + LOGGER.log(Level.INFO, "Participant reports it is still compensating."); + status.set(Status.CLIENT_COMPENSATING); + return false; + } else if (remainingCloseAttempts.decrementAndGet() <= 0) { + LOGGER.log(Level.INFO, "Participant didnt report final status after {0} status call retries.", + new Object[] {RETRY_CNT}); + status.set(Status.FAILED_TO_COMPENSATE); + return true; + } else { + // Unknown status, lets try in next recovery cycle + LOGGER.log(Level.INFO, "Unknown status of " + lra.lraId()); + return false; + } + } + + response = webClient.put() + .uri(endpointURI.get()) + .headers(lra.headers()) + .submit(LRAStatus.Cancelled.name()); + + // When timeout occur we loose track of the participant status + // next retry will attempt to retrieve participant status if status uri is available + + switch (response.status().code()) { + // complete or compensated + case 200: + case 410: + LOGGER.log(Level.INFO, "Compensated participant of LRA {0} {1}", + new Object[] {lra.lraId(), this.compensateURI()}); + status.set(Status.COMPENSATED); + compensateCalled.set(CompensateStatus.SENT); + return true; + + // retryable + case 202: + // Still compensating, check with @Status later + this.status.set(Status.CLIENT_COMPENSATING); + return false; + case 409: + case 404: + case 503: + default: + throw new Exception(response.status().code() + " " + response.status().reasonPhrase()); + } + + } catch (Exception e) { + LOGGER.log(Level.WARNING, + () -> "Can't reach participant's compensate endpoint: " + + endpointURI.map(URI::toASCIIString).orElse("unknown"), e); + if (remainingCloseAttempts.decrementAndGet() <= 0) { + LOGGER.log(Level.WARNING, "Failed to compensate participant of LRA {0} {1} {2}", + new Object[] {lra.lraId(), this.compensateURI(), e.getMessage()}); + status.set(Status.FAILED_TO_COMPENSATE); + } else { + status.set(Status.COMPENSATING); + } + + } finally { + Optional.ofNullable(response).ifPresent(HttpClientResponse::close); + sendingStatus.set(SendingStatus.NOT_SENDING); + compensateCalled.compareAndSet(CompensateStatus.SENDING, CompensateStatus.NOT_SENT); + } + } + return false; + } + + boolean sendComplete(LraImpl lra) { + Optional endpointURI = completeURI(); + for (AtomicInteger i = new AtomicInteger(0); i.getAndIncrement() < SYNCHRONOUS_RETRY_CNT;) { + if (!sendingStatus.compareAndSet(SendingStatus.NOT_SENDING, SendingStatus.SENDING)) { + return false; + } + LOGGER.log(Level.DEBUG, () -> "Sending complete, sync retry: " + i.get() + + ", status: " + status.get().name() + + " statusUri: " + statusURI().map(URI::toASCIIString).orElse(null)); + HttpClientResponse response = null; + try { + if (status.get().isFinal()) { + return true; + // call for client status only on retries and when status uri is known + } else if (!status.get().equals(Status.ACTIVE) && statusURI().isPresent()) { + // If the participant does not support idempotency then it MUST be able to report its status + // by annotating one of the methods with the @Status annotation which should report the status + // in case we can't retrieve status from participant just retry n times + ParticipantStatus reportedClientStatus = retrieveStatus(lra, Completing).orElse(null); + if (reportedClientStatus == Completed) { + LOGGER.log(Level.INFO, "Participant reports it is completed."); + status.set(Status.COMPLETED); + return true; + } else if (reportedClientStatus == FailedToComplete) { + LOGGER.log(Level.INFO, "Participant reports it failed to complete."); + status.set(Status.FAILED_TO_COMPLETE); + return true; + } else if (reportedClientStatus == Active) { + // last call didn't reach participant, try call again + } else if (reportedClientStatus == Completing) { + LOGGER.log(Level.INFO, "Participant reports it is still completing."); + status.set(Status.CLIENT_COMPLETING); + return false; + } else if (remainingCloseAttempts.decrementAndGet() <= 0) { + LOGGER.log(Level.INFO, "Participant didnt report final status after {0} status call retries.", + new Object[] {RETRY_CNT}); + status.set(Status.FAILED_TO_COMPLETE); + return true; + } else { + // Unknown status, lets try in next recovery cycle + return false; + } + } + response = webClient.put() + .uri(endpointURI.get()) + .headers(lra.headers()) + .submit(LRAStatus.Closed.name()); + // When timeout occur we loose track of the participant status + // next retry will attempt to retrieve participant status if status uri is available + + switch (response.status().code()) { + // complete or compensated + case 200: + case 410: + status.set(Status.COMPLETED); + return true; + + // retryable + case 202: + // Still completing, check with @Status later + this.status.set(Status.CLIENT_COMPLETING); + return false; + case 409: + case 404: + case 503: + default: + throw new Exception(response.status().code() + " " + response.status().reasonPhrase()); + } + + } catch (Exception e) { + LOGGER.log(Level.WARNING, + () -> "Can't reach participant's complete endpoint: " + endpointURI.map(URI::toASCIIString) + .orElse("unknown"), e); + if (remainingCloseAttempts.decrementAndGet() <= 0) { + LOGGER.log(Level.WARNING, "Failed to complete participant of LRA {0} {1} {2}", + new Object[] {lra.lraId(), + this.completeURI(), e.getMessage()}); + status.set(Status.FAILED_TO_COMPLETE); + } else { + status.set(Status.COMPLETING); + } + } finally { + Optional.ofNullable(response).ifPresent(HttpClientResponse::close); + sendingStatus.set(SendingStatus.NOT_SENDING); + } + } + return false; + } + + boolean trySendAfterLRA(LraImpl lra) { + for (int i = 0; i < SYNCHRONOUS_RETRY_CNT; i++) { + // Participant in right state + if (!isInEndStateOrListenerOnly()) { + return false; + } + // LRA in right state + if (!(Set.of(LRAStatus.Closed, LRAStatus.Cancelled).contains(lra.lraStatus().get()))) { + return false; + } + + HttpClientResponse response = null; + try { + Optional afterURI = afterURI(); + if (afterURI.isPresent() && afterLRACalled.compareAndSet(AfterLraStatus.NOT_SENT, AfterLraStatus.SENDING)) { + response = webClient.put() + .uri(afterURI.get()) + .headers(lra.headers()) + .submit(lra.lraStatus().get().name()); + + if (response.status().code() == 200) { + afterLRACalled.set(AfterLraStatus.SENT); + } else if (remainingAfterLraAttempts.decrementAndGet() <= 0) { + afterLRACalled.set(AfterLraStatus.SENT); + } + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error when sending after lra", e); + if (remainingAfterLraAttempts.decrementAndGet() <= 0) { + afterLRACalled.set(AfterLraStatus.SENT); + } else { + afterLRACalled.set(AfterLraStatus.NOT_SENT); + } + } finally { + Optional.ofNullable(response).ifPresent(HttpClientResponse::close); + } + if (afterLRACalled.get() == AfterLraStatus.SENT) { + return true; + } + } + return false; + } + + Optional retrieveStatus(LraImpl lra, ParticipantStatus inProgressStatus) { + URI statusURI = this.statusURI().get(); + try (HttpClientResponse response = webClient.get() + .uri(statusURI) + .headers(h -> { + // Dont send parent! + h.add(LRA_HTTP_CONTEXT_HEADER_NAME, lra.lraContextId()); + h.add(LRA_HTTP_RECOVERY_HEADER_NAME, lra.lraContextId() + "/recovery"); + h.add(LRA_HTTP_ENDED_CONTEXT_HEADER_NAME, lra.lraContextId()); + }) + .request()) { + + int code = response.status().code(); + switch (code) { + case 202: + return Optional.of(inProgressStatus); + case 410: //GONE + //Completing -> FailedToComplete ... + return status.get().failedFinalStatus(); + case 503: + case 500: + throw new IllegalStateException(String.format("Client reports unexpected status %s %s, " + + "current participant state is %s, " + + "lra: %s " + + "status uri: %s", + code, + response.as(String.class), + status.get(), + lra.lraId(), + statusURI.toASCIIString())); + default: + ParticipantStatus reportedStatus = valueOf(response.as(String.class)); + Status currentStatus = status.get(); + if (currentStatus.validateNextStatus(reportedStatus)) { + return Optional.of(reportedStatus); + } else { + LOGGER.log(Level.WARNING, + "Client reports unexpected status {0} {1}, " + + "current participant state is {2}, " + + "lra: {3} " + + "status uri: {4}", + new Object[] {code, reportedStatus, currentStatus, lra.lraId(), statusURI.toASCIIString()}); + return Optional.empty(); + } + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error when getting participant status. " + statusURI, e); + // skip dependent compensation call, another retry with status call might be luckier + throw e; + } + } + + boolean sendForget(LraImpl lra) { + if (!forgetCalled.compareAndSet(ForgetStatus.NOT_SENT, ForgetStatus.SENDING)) { + return false; + } + try ( + HttpClientResponse response = webClient.delete() + .uri(forgetURI().get()) + .headers(lra.headers()) + .request()) { + + int responseStatus = response.status().code(); + if (responseStatus == 200 || responseStatus == 410) { + forgetCalled.set(ForgetStatus.SENT); + } else { + throw new Exception("Unexpected response from participant " + response.status().code()); + } + } catch (Throwable e) { + LOGGER.log(Level.WARNING, "Unable to send forget of lra {0} to {1}", + new Object[] {lra.lraId(), forgetURI().get()}); + forgetCalled.set(ForgetStatus.NOT_SENT); + } + return forgetCalled.get() == ForgetStatus.SENT; + } + + boolean equalCompensatorUris(String compensatorUris) { + Set links = Arrays.stream(compensatorUris.split(",")) + .map(Link::valueOf) + .collect(Collectors.toSet()); + + for (Link link : links) { + Optional participantsLink = compensatorLink(link.rel()); + if (participantsLink.isEmpty()) { + continue; + } + + if (Objects.equals(participantsLink.get(), link.uri())) { + return true; + } + } + + return false; + } + + enum Status { + ACTIVE(Active, null, null, false, Set.of(Completing, Compensating)), + + COMPENSATED(Compensated, null, null, true, Set.of(Compensated)), + COMPLETED(Completed, null, null, true, Set.of(Completed)), + FAILED_TO_COMPENSATE(FailedToCompensate, null, null, true, Set.of()), + FAILED_TO_COMPLETE(FailedToComplete, null, null, true, Set.of()), + + CLIENT_COMPENSATING(Compensating, COMPENSATED, FAILED_TO_COMPENSATE, false, + Set.of(Active, Compensated, FailedToCompensate)), + CLIENT_COMPLETING(Completing, COMPLETED, FAILED_TO_COMPLETE, false, Set.of(Active, Completed, FailedToComplete)), + COMPENSATING(Compensating, COMPENSATED, FAILED_TO_COMPENSATE, false, Set.of(Active, Compensated, FailedToCompensate)), + COMPLETING(Completing, COMPLETED, FAILED_TO_COMPLETE, false, Set.of(Active, Completed, FailedToComplete)); + + private final ParticipantStatus participantStatus; + private final Status successFinalStatus; + private final Status failedFinalStatus; + private final boolean finalState; + private final Set validNextStates; + + Status(ParticipantStatus participantStatus, + Status successFinalStatus, + Status failedFinalStatus, + boolean finalState, + Set validNextStates) { + this.participantStatus = participantStatus; + this.successFinalStatus = successFinalStatus; + this.failedFinalStatus = failedFinalStatus; + this.finalState = finalState; + this.validNextStates = validNextStates; + } + + ParticipantStatus participantStatus() { + return participantStatus; + } + + boolean isFinal() { + return finalState; + } + + boolean validateNextStatus(ParticipantStatus participantStatus) { + return validNextStates.contains(participantStatus); + } + + Optional successFinalStatus() { + return Optional.ofNullable(successFinalStatus.participantStatus()); + } + + Optional failedFinalStatus() { + return Optional.ofNullable(failedFinalStatus.participantStatus); + } + } + + enum SendingStatus { + SENDING, NOT_SENDING; + } + + enum AfterLraStatus { + NOT_SENT, SENDING, SENT; + } + + enum ForgetStatus { + NOT_SENT, SENDING, SENT; + } + + enum CompensateStatus { + NOT_SENT, SENDING, SENT; + } +} diff --git a/lra/coordinator/server/src/main/java/module-info.java b/lra/coordinator/server/src/main/java/module-info.java index 41fb4c00841..e97b6dafb7c 100644 --- a/lra/coordinator/server/src/main/java/module-info.java +++ b/lra/coordinator/server/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ * Helidon LRA coordinator. */ module io.helidon.lra.coordinator { + exports io.helidon.lra.coordinator; requires io.helidon.dbclient.jdbc; requires io.helidon.dbclient; diff --git a/lra/pom.xml b/lra/pom.xml index 3f4e71c6b33..2601a99f13f 100644 --- a/lra/pom.xml +++ b/lra/pom.xml @@ -24,7 +24,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.lra helidon-lra-project diff --git a/messaging/connectors/aq/pom.xml b/messaging/connectors/aq/pom.xml index 0b7a51a630e..38ebfa0e6be 100644 --- a/messaging/connectors/aq/pom.xml +++ b/messaging/connectors/aq/pom.xml @@ -24,7 +24,7 @@ io.helidon.messaging helidon-messaging-connectors-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.messaging.aq diff --git a/messaging/connectors/jms-shim/pom.xml b/messaging/connectors/jms-shim/pom.xml index 3e368497f6c..110e94e9348 100644 --- a/messaging/connectors/jms-shim/pom.xml +++ b/messaging/connectors/jms-shim/pom.xml @@ -24,7 +24,7 @@ io.helidon.messaging helidon-messaging-connectors-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-messaging-jms-shim diff --git a/messaging/connectors/jms/pom.xml b/messaging/connectors/jms/pom.xml index bbe8b062535..7f734118910 100644 --- a/messaging/connectors/jms/pom.xml +++ b/messaging/connectors/jms/pom.xml @@ -24,7 +24,7 @@ io.helidon.messaging helidon-messaging-connectors-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.messaging.jms diff --git a/messaging/connectors/kafka/pom.xml b/messaging/connectors/kafka/pom.xml index 4dd8f1870cf..be093582dc1 100644 --- a/messaging/connectors/kafka/pom.xml +++ b/messaging/connectors/kafka/pom.xml @@ -25,7 +25,7 @@ io.helidon.messaging helidon-messaging-connectors-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.messaging.kafka diff --git a/messaging/connectors/mock/pom.xml b/messaging/connectors/mock/pom.xml index aaf467f8859..8e1856cbed9 100644 --- a/messaging/connectors/mock/pom.xml +++ b/messaging/connectors/mock/pom.xml @@ -24,7 +24,7 @@ io.helidon.messaging helidon-messaging-connectors-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.messaging.mock diff --git a/messaging/connectors/pom.xml b/messaging/connectors/pom.xml index b228af13ea7..48b997b6ad2 100644 --- a/messaging/connectors/pom.xml +++ b/messaging/connectors/pom.xml @@ -23,7 +23,7 @@ io.helidon.messaging helidon-messaging-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-messaging-connectors-project Helidon Messaging Connectors Project diff --git a/messaging/connectors/wls-jms/pom.xml b/messaging/connectors/wls-jms/pom.xml index fb06c76e3e9..67ec92d5f3c 100644 --- a/messaging/connectors/wls-jms/pom.xml +++ b/messaging/connectors/wls-jms/pom.xml @@ -24,7 +24,7 @@ io.helidon.messaging helidon-messaging-connectors-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.messaging.wls-jms diff --git a/messaging/messaging/pom.xml b/messaging/messaging/pom.xml index df8cc937c15..b343b78c65a 100644 --- a/messaging/messaging/pom.xml +++ b/messaging/messaging/pom.xml @@ -23,7 +23,7 @@ io.helidon.messaging helidon-messaging-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-messaging diff --git a/messaging/pom.xml b/messaging/pom.xml index 76b2371a4e9..e94df6126c9 100644 --- a/messaging/pom.xml +++ b/messaging/pom.xml @@ -24,7 +24,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.messaging helidon-messaging-project diff --git a/metadata/hson/pom.xml b/metadata/hson/pom.xml index 95d50b2e4a5..5eacc7d13e3 100644 --- a/metadata/hson/pom.xml +++ b/metadata/hson/pom.xml @@ -22,7 +22,7 @@ io.helidon.metadata helidon-metadata-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-metadata-hson diff --git a/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonValues.java b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonValues.java index 6c94cb8e497..54095e99492 100644 --- a/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonValues.java +++ b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonValues.java @@ -76,13 +76,15 @@ private String quote(String value) { } private String escape(String string) { - return string.replaceAll("\n", "\\\\n") - .replaceAll("\"", "\\\\\"") - .replaceAll("\t", "\\\\\t") - .replaceAll("\r", "\\\\\r") - // replace two backslashes with four backslashes - .replaceAll("\\\\\\\\", "\\\\\\\\\\\\\\\\") - .replaceAll("\f", "\\\\\f"); + String result = string.replaceAll("\n", "\\\\n"); + + result = result.replaceAll("\"", "\\\\\""); + result = result.replaceAll("\t", "\\\\t"); + result = result.replaceAll("\r", "\\\\r"); + // replace two backslashes with four backslashes + result = result.replaceAll("\\\\\\\\", "\\\\\\\\\\\\\\\\"); + result = result.replaceAll("\f", "\\\\f"); + return result; } } diff --git a/metadata/hson/src/test/java/io/helidon/metadata/hson/ExistingTypesTest.java b/metadata/hson/src/test/java/io/helidon/metadata/hson/ExistingTypesTest.java index 9d5cf77d9fa..357fa7b092c 100644 --- a/metadata/hson/src/test/java/io/helidon/metadata/hson/ExistingTypesTest.java +++ b/metadata/hson/src/test/java/io/helidon/metadata/hson/ExistingTypesTest.java @@ -33,6 +33,16 @@ import static org.hamcrest.collection.IsCollectionWithSize.hasSize; class ExistingTypesTest { + @Test + void testNewServiceRegistry() throws IOException { + Hson.Array modules; + try (InputStream inputStream = resource("/new-service-registry.json")) { + assertThat(inputStream, notNullValue()); + modules = Hson.parse(inputStream) + .asArray(); + } + assertThat(modules, notNullValue()); + } @Test void testServiceRegistry() throws IOException { Hson.Struct object; diff --git a/metadata/hson/src/test/resources/new-service-registry.json b/metadata/hson/src/test/resources/new-service-registry.json new file mode 100644 index 00000000000..de9d76b7038 --- /dev/null +++ b/metadata/hson/src/test/resources/new-service-registry.json @@ -0,0 +1,16 @@ +[ + { + "module": "unnamed/io.helidon.metrics", + "services": [ + { + "type": "inject", + "weight": 90.0, + "descriptor": "io.helidon.metrics.CountedInterceptor__ServiceDescriptor", + "contracts": [ + "io.helidon.metrics.CountedInterceptor", + "io.helidon.service.inject.api.Interception.Interceptor" + ] + } + ] + } +] \ No newline at end of file diff --git a/metadata/pom.xml b/metadata/pom.xml index 0926ca062d2..36a18f6ab47 100644 --- a/metadata/pom.xml +++ b/metadata/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/metrics/api/pom.xml b/metrics/api/pom.xml index 0f0d4a86372..b5e17fa81ab 100644 --- a/metrics/api/pom.xml +++ b/metrics/api/pom.xml @@ -22,7 +22,7 @@ io.helidon.metrics helidon-metrics-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-metrics-api diff --git a/metrics/metrics/pom.xml b/metrics/metrics/pom.xml index adba141799b..cb7a8a4989c 100644 --- a/metrics/metrics/pom.xml +++ b/metrics/metrics/pom.xml @@ -24,7 +24,7 @@ io.helidon.metrics helidon-metrics-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-metrics Helidon Metrics diff --git a/metrics/pom.xml b/metrics/pom.xml index 3a2474738f6..40717afbf18 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.metrics diff --git a/metrics/prometheus/pom.xml b/metrics/prometheus/pom.xml index f19cd578d34..ab87f257bc5 100644 --- a/metrics/prometheus/pom.xml +++ b/metrics/prometheus/pom.xml @@ -24,7 +24,7 @@ io.helidon.metrics helidon-metrics-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-metrics-prometheus Helidon Metrics Prometheus diff --git a/metrics/provider-tests/pom.xml b/metrics/provider-tests/pom.xml index efb9dce4511..b4a44b13c2e 100644 --- a/metrics/provider-tests/pom.xml +++ b/metrics/provider-tests/pom.xml @@ -23,7 +23,7 @@ io.helidon.metrics helidon-metrics-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-metrics-provider-tests Helidon Metrics Provider Tests diff --git a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/SimpleMeterRegistryTests.java b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/SimpleMeterRegistryTests.java index 86a93dcf45e..5335fa74a30 100644 --- a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/SimpleMeterRegistryTests.java +++ b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/SimpleMeterRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,16 @@ package io.helidon.metrics.provider.tests; import java.util.List; +import java.util.Map; +import io.helidon.common.testing.junit5.OptionalMatcher; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; import io.helidon.metrics.api.Counter; import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.MetricsFactory; import io.helidon.metrics.api.Tag; import io.helidon.metrics.api.Timer; @@ -28,7 +34,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; @@ -78,4 +86,38 @@ void testSameNameSameTwoTags() { .tags(tags)); assertThat("Counter with same name, same two tags", counter1, is(sameInstance(counter2))); } + + @Test + void testDisabledYieldsNoOp() { + // Disable metrics using config. + Config metricsDisabledConfig = Config.just(ConfigSources.create(Map.of("enabled", "false"))); + MeterRegistry shouldBeNoOp = MetricsFactory.getInstance() + .createMeterRegistry(MetricsConfig.create(metricsDisabledConfig)); + + Counter shouldBeNoOpCounter = shouldBeNoOp.getOrCreate(Counter.builder("shouldBeNoOpCounter")); + assertThat("Counters after registration", shouldBeNoOp.meters(), is(emptyIterable())); + + shouldBeNoOpCounter.increment(); + assertThat("Counter value after increment", shouldBeNoOpCounter.count(), is(0L)); + } + + @Test + void testDisabledMeters() { + Config config = Config.just(ConfigSources.create(Map.of("scoping.scopes.0.name", "application", + "scoping.scopes.0.filter.exclude", ".*Ignore.*"))); + MeterRegistry selectiveRegistry = MetricsFactory.getInstance().createMeterRegistry(MetricsConfig.create(config)); + + Counter shouldBeNoOpCounter = selectiveRegistry.getOrCreate(Counter.builder("pleaseIgnoreThis")); + Counter shouldBeLive = selectiveRegistry.getOrCreate(Counter.builder("pleaseIncludeThis")); + assertThat("Counters after ignored registration", selectiveRegistry.meters(), hasSize(1)); + assertThat("Counter retrieved", + selectiveRegistry.counter("pleaseIncludeThis", List.of()), + OptionalMatcher.optionalPresent()); + + shouldBeNoOpCounter.increment(); + shouldBeLive.increment(); + + assertThat("Incremented disabled counter", shouldBeNoOpCounter.count(), is(0L)); + assertThat("Incremented live counter", shouldBeLive.count(), is(1L)); + } } diff --git a/metrics/providers/micrometer/pom.xml b/metrics/providers/micrometer/pom.xml index 89847d75cce..c0d1f1f6a02 100644 --- a/metrics/providers/micrometer/pom.xml +++ b/metrics/providers/micrometer/pom.xml @@ -24,7 +24,7 @@ io.helidon.metrics.providers helidon-metrics-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-metrics-providers-micrometer Helidon Metrics Providers Micrometer @@ -47,6 +47,10 @@ io.micrometer micrometer-registry-prometheus + + io.micrometer + micrometer-registry-prometheus-simpleclient + io.helidon.config helidon-config diff --git a/metrics/providers/micrometer/src/main/java/module-info.java b/metrics/providers/micrometer/src/main/java/module-info.java index a76661d745b..d3361b87bd0 100644 --- a/metrics/providers/micrometer/src/main/java/module-info.java +++ b/metrics/providers/micrometer/src/main/java/module-info.java @@ -21,6 +21,7 @@ requires io.helidon.metrics.api; requires micrometer.core; requires static micrometer.registry.prometheus; + requires static micrometer.registry.prometheus.simpleclient; requires io.helidon.common; requires io.helidon.common.media.type; requires io.helidon.config; diff --git a/metrics/providers/pom.xml b/metrics/providers/pom.xml index 8f700fdc789..fc7a7f1cb4e 100644 --- a/metrics/providers/pom.xml +++ b/metrics/providers/pom.xml @@ -22,7 +22,7 @@ io.helidon.metrics helidon-metrics-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.metrics.providers diff --git a/metrics/system-meters/pom.xml b/metrics/system-meters/pom.xml index b9f997a8b83..bd17ebc3dad 100644 --- a/metrics/system-meters/pom.xml +++ b/metrics/system-meters/pom.xml @@ -22,7 +22,7 @@ io.helidon.metrics helidon-metrics-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-metrics-system-meters diff --git a/metrics/trace-exemplar/pom.xml b/metrics/trace-exemplar/pom.xml index 79078238d1e..d4c18a3d24a 100644 --- a/metrics/trace-exemplar/pom.xml +++ b/metrics/trace-exemplar/pom.xml @@ -23,7 +23,7 @@ helidon-metrics-project io.helidon.metrics - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/microprofile/access-log/pom.xml b/microprofile/access-log/pom.xml index f5a2cb6bf52..ce7c9df9741 100644 --- a/microprofile/access-log/pom.xml +++ b/microprofile/access-log/pom.xml @@ -23,7 +23,7 @@ helidon-microprofile-project io.helidon.microprofile - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/microprofile/bean-validation/pom.xml b/microprofile/bean-validation/pom.xml index aa3cfed9bb9..a0d46f024aa 100644 --- a/microprofile/bean-validation/pom.xml +++ b/microprofile/bean-validation/pom.xml @@ -21,7 +21,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.bean-validation helidon-microprofile-bean-validation diff --git a/microprofile/bundles/helidon-microprofile-core/pom.xml b/microprofile/bundles/helidon-microprofile-core/pom.xml index 643efbdcbb6..0cbec512486 100644 --- a/microprofile/bundles/helidon-microprofile-core/pom.xml +++ b/microprofile/bundles/helidon-microprofile-core/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.bundles bundles-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-core Helidon Microprofile Core Bundle diff --git a/microprofile/bundles/helidon-microprofile/pom.xml b/microprofile/bundles/helidon-microprofile/pom.xml index 0bd3e8d1e0d..bd1ba22548e 100644 --- a/microprofile/bundles/helidon-microprofile/pom.xml +++ b/microprofile/bundles/helidon-microprofile/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.bundles bundles-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile Helidon Microprofile Full Bundle diff --git a/microprofile/bundles/pom.xml b/microprofile/bundles/pom.xml index 8b2141ab83f..2bace9c9b88 100644 --- a/microprofile/bundles/pom.xml +++ b/microprofile/bundles/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.microprofile.bundles diff --git a/microprofile/cdi/pom.xml b/microprofile/cdi/pom.xml index c8b0dd515e9..598c0dca03d 100644 --- a/microprofile/cdi/pom.xml +++ b/microprofile/cdi/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.cdi diff --git a/microprofile/config/pom.xml b/microprofile/config/pom.xml index 799e2a2ec5d..089c16a6418 100644 --- a/microprofile/config/pom.xml +++ b/microprofile/config/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.config helidon-microprofile-config diff --git a/microprofile/cors/pom.xml b/microprofile/cors/pom.xml index 64d244c88e8..58d47ba1779 100644 --- a/microprofile/cors/pom.xml +++ b/microprofile/cors/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-cors diff --git a/microprofile/fault-tolerance/pom.xml b/microprofile/fault-tolerance/pom.xml index 895c13bb7d3..4a712031e1e 100644 --- a/microprofile/fault-tolerance/pom.xml +++ b/microprofile/fault-tolerance/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-fault-tolerance Helidon Microprofile Fault Tolerance diff --git a/microprofile/graphql/pom.xml b/microprofile/graphql/pom.xml index 99cfb08e03f..8d28d5d2363 100644 --- a/microprofile/graphql/pom.xml +++ b/microprofile/graphql/pom.xml @@ -20,7 +20,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/microprofile/graphql/server/pom.xml b/microprofile/graphql/server/pom.xml index 4ad742156e0..46d0ff2029b 100644 --- a/microprofile/graphql/server/pom.xml +++ b/microprofile/graphql/server/pom.xml @@ -20,7 +20,7 @@ io.helidon.microprofile.graphql helidon-microprofile-graphql - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/microprofile/grpc/client/pom.xml b/microprofile/grpc/client/pom.xml index f2b2c481fba..ef9c623363b 100644 --- a/microprofile/grpc/client/pom.xml +++ b/microprofile/grpc/client/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.grpc helidon-microprofile-grpc-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-grpc-client diff --git a/microprofile/grpc/core/pom.xml b/microprofile/grpc/core/pom.xml index 1627be21150..af369256f98 100644 --- a/microprofile/grpc/core/pom.xml +++ b/microprofile/grpc/core/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.grpc helidon-microprofile-grpc-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-grpc-core diff --git a/microprofile/grpc/pom.xml b/microprofile/grpc/pom.xml index 99bf0a4e4ab..87accbcf72f 100644 --- a/microprofile/grpc/pom.xml +++ b/microprofile/grpc/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.grpc diff --git a/microprofile/grpc/server/pom.xml b/microprofile/grpc/server/pom.xml index 443eaac5759..3dc3885c9a5 100644 --- a/microprofile/grpc/server/pom.xml +++ b/microprofile/grpc/server/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.grpc helidon-microprofile-grpc-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-grpc-server diff --git a/microprofile/grpc/tests/pom.xml b/microprofile/grpc/tests/pom.xml index d2ecf9306b2..724420758e3 100644 --- a/microprofile/grpc/tests/pom.xml +++ b/microprofile/grpc/tests/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.grpc helidon-microprofile-grpc-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-grpc-tests diff --git a/microprofile/grpc/tracing/pom.xml b/microprofile/grpc/tracing/pom.xml index ecfc63cdb6d..27ffe3b9347 100644 --- a/microprofile/grpc/tracing/pom.xml +++ b/microprofile/grpc/tracing/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.grpc helidon-microprofile-grpc-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-grpc-tracing diff --git a/microprofile/health/pom.xml b/microprofile/health/pom.xml index e504fe15795..73bf423f707 100644 --- a/microprofile/health/pom.xml +++ b/microprofile/health/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.health helidon-microprofile-health diff --git a/microprofile/jwt-auth/pom.xml b/microprofile/jwt-auth/pom.xml index faf7b10dea6..8c74fd1e7d7 100644 --- a/microprofile/jwt-auth/pom.xml +++ b/microprofile/jwt-auth/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.jwt diff --git a/microprofile/lra/jax-rs/pom.xml b/microprofile/lra/jax-rs/pom.xml index d1676e48b8c..8d7842ff389 100644 --- a/microprofile/lra/jax-rs/pom.xml +++ b/microprofile/lra/jax-rs/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.lra helidon-microprofile-lra-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-lra diff --git a/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/LraCdiExtension.java b/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/LraCdiExtension.java index 26f4e2d8ef8..6fcea662735 100644 --- a/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/LraCdiExtension.java +++ b/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/LraCdiExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,8 @@ import java.util.stream.Stream; import io.helidon.common.Reflected; +import io.helidon.config.Config; +import io.helidon.config.mp.MpConfig; import io.helidon.microprofile.server.ServerCdiExtension; import io.helidon.webserver.http.HttpService; @@ -60,6 +62,7 @@ import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Application; +import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.lra.annotation.AfterLRA; import org.eclipse.microprofile.lra.annotation.Compensate; import org.eclipse.microprofile.lra.annotation.Complete; @@ -84,6 +87,7 @@ public class LraCdiExtension implements Extension { private static final System.Logger LOGGER = System.getLogger(LraCdiExtension.class.getName()); + private static final String CONFIG_PREFIX = "helidon.lra.participant"; private static final Set> EXPECTED_ANNOTATIONS = Set.of( AfterLRA.class, @@ -98,6 +102,7 @@ public class LraCdiExtension implements Extension { private final Map, Bean> lraCdiBeanReferences = new HashMap<>(); private final Indexer indexer; private final ClassLoader classLoader; + private final Config config; private IndexView index; @@ -105,6 +110,7 @@ public class LraCdiExtension implements Extension { * Initialize MicroProfile Long Running Actions CDI extension. */ public LraCdiExtension() { + config = MpConfig.toHelidonConfig(ConfigProvider.getConfig()).get(CONFIG_PREFIX); indexer = new Indexer(); classLoader = Thread.currentThread().getContextClassLoader(); // Needs to be always indexed @@ -117,17 +123,9 @@ public LraCdiExtension() { Application.class, NonJaxRsResource.class).forEach(c -> runtimeIndex(DotName.createSimple(c.getName()))); - List indexFiles; - try { - indexFiles = findIndexFiles("META-INF/jandex.idx"); - if (!indexFiles.isEmpty()) { - index = CompositeIndex.create(indexer.complete(), existingIndexFileReader(indexFiles)); - } else { - index = null; - } - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Error when locating Jandex index, fall-back to runtime computed index.", e); - index = null; + Boolean useBuildTimeIndex = config.get("use-build-time-index").asBoolean().orElse(Boolean.TRUE); + if (useBuildTimeIndex) { + resolveBuildTimeIndex(); } } @@ -139,7 +137,7 @@ private void index( LRA.class, AfterLRA.class, Compensate.class, Complete.class, Forget.class, Status.class }) ProcessAnnotatedType pat) { - // compile time bilt index + // compile time built index if (index != null) return; // create runtime index when pre-built index is not available runtimeIndex(DotName.createSimple(pat.getAnnotatedType().getJavaClass().getName())); @@ -278,6 +276,21 @@ void runtimeIndex(DotName fqdn) { } } + private void resolveBuildTimeIndex() { + List indexFiles; + try { + indexFiles = findIndexFiles(config.get("index-resource").asString().orElse("META-INF/jandex.idx")); + if (!indexFiles.isEmpty()) { + index = CompositeIndex.create(indexer.complete(), existingIndexFileReader(indexFiles)); + } else { + index = null; + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error when locating Jandex index, fall-back to runtime computed index.", e); + index = null; + } + } + private IndexView existingIndexFileReader(List indexUrls) throws IOException { List indices = new ArrayList<>(); for (URL indexURL : indexUrls) { diff --git a/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/ParticipantImpl.java b/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/ParticipantImpl.java index 923e1a9b418..cc2b0355093 100644 --- a/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/ParticipantImpl.java +++ b/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/ParticipantImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; import io.helidon.lra.coordinator.client.Participant; @@ -150,9 +149,9 @@ public Optional status() { static Optional getLRAAnnotation(Method m) { List found = Arrays.stream(m.getDeclaredAnnotations()) .filter(a -> LRA_ANNOTATIONS.contains(a.annotationType())) - .collect(Collectors.toList()); + .toList(); - if (found.size() == 0) { + if (found.isEmpty()) { // LRA can be inherited from class or its predecessors var clazz = m.getDeclaringClass(); do { diff --git a/microprofile/lra/jax-rs/src/main/java/module-info.java b/microprofile/lra/jax-rs/src/main/java/module-info.java index 1d778858091..df670692c40 100644 --- a/microprofile/lra/jax-rs/src/main/java/module-info.java +++ b/microprofile/lra/jax-rs/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ ) @SuppressWarnings({ "requires-automatic", "requires-transitive-automatic" }) module io.helidon.microprofile.lra { + exports io.helidon.microprofile.lra; requires io.helidon.common.reactive; requires io.helidon.config; @@ -46,6 +47,7 @@ requires jakarta.cdi; requires transitive jersey.common; + requires io.helidon.config.mp; uses io.helidon.lra.coordinator.client.CoordinatorClient; diff --git a/microprofile/lra/pom.xml b/microprofile/lra/pom.xml index 1448ff3df54..8ff245a2f44 100644 --- a/microprofile/lra/pom.xml +++ b/microprofile/lra/pom.xml @@ -1,7 +1,7 @@ + + + 4.0.0 + + io.helidon.microprofile.lra + helidon-microprofile-lra-project + 4.2.0-SNAPSHOT + ../pom.xml + + + helidon-microprofile-lra-testing + Helidon Microprofile LRA Testing + + + LRA test support + + + + + io.helidon.microprofile.server + helidon-microprofile-server + + + io.helidon.microprofile.lra + helidon-microprofile-lra + + + io.helidon.lra + helidon-lra-coordinator-narayana-client + + + io.helidon.lra + helidon-lra-coordinator-server + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.logging + helidon-logging-jul + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + diff --git a/microprofile/lra/testing/src/main/java/io/helidon/microprofile/testing/lra/TestLraCoordinator.java b/microprofile/lra/testing/src/main/java/io/helidon/microprofile/testing/lra/TestLraCoordinator.java new file mode 100644 index 00000000000..e3c6bf3a4da --- /dev/null +++ b/microprofile/lra/testing/src/main/java/io/helidon/microprofile/testing/lra/TestLraCoordinator.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.testing.lra; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import io.helidon.config.Config; +import io.helidon.lra.coordinator.CoordinatorService; +import io.helidon.lra.coordinator.Lra; +import io.helidon.microprofile.lra.CoordinatorLocatorService; +import io.helidon.microprofile.server.RoutingName; +import io.helidon.microprofile.server.RoutingPath; +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.webserver.http.HttpService; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Initialized; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; + +import static jakarta.interceptor.Interceptor.Priority.PLATFORM_AFTER; +import static java.util.concurrent.TimeUnit.SECONDS; + +/** + * Enables LRA coordinator on another socket of this server with random port. + */ +@ApplicationScoped +public class TestLraCoordinator { + + static final String ROUTING_NAME = "test-lra-coordinator"; + static final String CONTEXT_PATH = "/lra-coordinator"; + private final CompletableFuture port = new CompletableFuture<>(); + private final ServerCdiExtension serverCdiExtension; + private final CoordinatorService coordinatorService; + + @Inject + TestLraCoordinator(Config config, + ServerCdiExtension serverCdiExtension, + CoordinatorLocatorService coordinatorLocator) { + this.serverCdiExtension = serverCdiExtension; + this.coordinatorService = CoordinatorService.builder() + .url(this::coordinatorUri) + .config(config.get(CoordinatorService.CONFIG_PREFIX)) + .build(); + coordinatorLocator.overrideCoordinatorUriSupplier(this::coordinatorUri); + } + + @Produces + @ApplicationScoped + @RoutingName(value = ROUTING_NAME, required = true) + @RoutingPath(CONTEXT_PATH) + HttpService coordinatorService() { + return coordinatorService; + } + + /** + * Return test LRA coordinator URL. + * + * @return coordinator url + */ + public URI coordinatorUri() { + return URI.create("http://localhost:" + awaitPort() + CONTEXT_PATH); + } + + /** + * Get LRA by LraId. + * + * @param lraId with or without coordinator url prefix. + * @return lra when registered by text coordinator + */ + public Lra lra(String lraId) { + if (lraId == null) { + return null; + } + if (lraId.startsWith(coordinatorUri() + "/")) { + return coordinatorService.lra(lraId.substring(coordinatorUri().toString().length() + 1)); + } + return coordinatorService.lra(lraId); + } + + private void ready(@Observes @Priority(PLATFORM_AFTER + 101) @Initialized(ApplicationScoped.class) Object e, BeanManager b) { + port.complete(serverCdiExtension.port(ROUTING_NAME)); + } + + private int awaitPort() { + try { + return port.get(20, SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + } +} diff --git a/microprofile/lra/testing/src/main/java/io/helidon/microprofile/testing/lra/TestLraCoordinatorConfigSource.java b/microprofile/lra/testing/src/main/java/io/helidon/microprofile/testing/lra/TestLraCoordinatorConfigSource.java new file mode 100644 index 00000000000..ad1a1e89f7e --- /dev/null +++ b/microprofile/lra/testing/src/main/java/io/helidon/microprofile/testing/lra/TestLraCoordinatorConfigSource.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.testing.lra; + +import java.util.Map; +import java.util.Set; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +/** + * Configuration for {@code @HelidonTest} with LRA coordinator running on random port. + * Any of the properties can be overridden with {@code @AddConfig}. + * Example of running test coordinator on port 8070: + *

{@code
+ * @HelidonTest
+ * @AddConfig(key = "server.sockets.500.port", value = "8070")
+ * @AddBean(TestLraCoordinator.class)
+ * }
+ */ +public class TestLraCoordinatorConfigSource implements ConfigSource { + + private static final String PORT_IDX = System.getProperty("helidon.lra.coordinator.test-socket.index", "500"); + private static final Map CONFIG = Map.of( + // Extra socket for coordinator on random port + "server.sockets." + PORT_IDX + ".name", TestLraCoordinator.ROUTING_NAME, + "server.sockets." + PORT_IDX + ".port", "0", + "server.sockets." + PORT_IDX + ".bind-address", "localhost", + // Avoid using persistent tx log in test LRA coordinator + "helidon.lra.coordinator.persistence", "false", + // Avoid using build time Jandex index + "helidon.lra.participant.use-build-time-index", "false"); + + /** + * Initialized by service locator. + */ + public TestLraCoordinatorConfigSource() { + } + + @Override + public Set getPropertyNames() { + return CONFIG.keySet(); + } + + @Override + public int getOrdinal() { + return 5000; + } + + @Override + public String getValue(String propertyName) { + return CONFIG.get(propertyName); + } + + @Override + public String getName() { + return TestLraCoordinator.ROUTING_NAME; + } +} diff --git a/microprofile/lra/testing/src/main/java/io/helidon/microprofile/testing/lra/package-info.java b/microprofile/lra/testing/src/main/java/io/helidon/microprofile/testing/lra/package-info.java new file mode 100644 index 00000000000..a1737087e36 --- /dev/null +++ b/microprofile/lra/testing/src/main/java/io/helidon/microprofile/testing/lra/package-info.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Test LRA coordinator. + * Allows simplified testing of LRA enabled JAX-RS resources. + * + *
{@code
+ * @HelidonTest
+ * @AddBean(TestLraCoordinator.class)
+ * @Path("/test")
+ * public class LraTest {
+ *
+ *     private final WebTarget target;
+ *     private final Set completedLras;
+ *     private final Set cancelledLras;
+ *     private final TestLraCoordinator coordinator;
+ *
+ *     @Inject
+ *     public LraTest(WebTarget target,
+ *                    TestLraCoordinator coordinator) {
+ *         this.target = target;
+ *         this.coordinator = coordinator;
+ *         this.completedLras = new CopyOnWriteArraySet<>();
+ *         this.cancelledLras = new CopyOnWriteArraySet<>();
+ *     }
+ *
+ *     @PUT
+ *     @Path("/withdraw")
+ *     @LRA(LRA.Type.REQUIRES_NEW)
+ *     public Response withdraw(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) Optional lraId, String content) {
+ *         ...
+ *         return Response.ok().build();
+ *     }
+ *
+ *     @Complete
+ *     public void complete(URI lraId) {
+ *         completedLras.add(lraId.toString());
+ *     }
+ *
+ *     @Compensate
+ *     public void rollback(URI lraId) {
+ *         cancelledLras.add(lraId.toString());
+ *     }
+ *
+ *     @Test
+ *     public void testLra() {
+ *         try (Response res = target
+ *                 .path("/test/withdraw")
+ *                 .request()
+ *                 .put(Entity.entity("test", MediaType.TEXT_PLAIN_TYPE))) {
+ *             assertThat(res.getStatus(), is(200));
+ *             String lraId = res.getHeaderString(LRA.LRA_HTTP_CONTEXT_HEADER);
+ *             Lra lra = coordinator.lra(lraId);
+ *             assertThat(lra.status(), is(LRAStatus.Closed));
+ *             assertThat(completedLras, contains(lraId));
+ *         }
+ *     }
+ * }
+ * }
+ */ +package io.helidon.microprofile.testing.lra; diff --git a/microprofile/lra/testing/src/main/java/module-info.java b/microprofile/lra/testing/src/main/java/module-info.java new file mode 100644 index 00000000000..d6c8c402868 --- /dev/null +++ b/microprofile/lra/testing/src/main/java/module-info.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.eclipse.microprofile.config.spi.ConfigSource; + +/** + * Test LRA coordinator. + */ +module io.helidon.microprofile.testing.lra { + + requires io.helidon.config.mp; + requires io.helidon.config.yaml.mp; + requires io.helidon.microprofile.cdi; + requires jakarta.inject; + + requires transitive jakarta.cdi; + requires transitive jakarta.ws.rs; + + requires static io.helidon.microprofile.server; + requires io.helidon.lra.coordinator; + requires io.helidon.microprofile.lra; + + exports io.helidon.microprofile.testing.lra; + + provides org.eclipse.microprofile.config.spi.ConfigSource + with io.helidon.microprofile.testing.lra.TestLraCoordinatorConfigSource; + +} diff --git a/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/LraDisabledDiscoveryResourceTest.java b/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/LraDisabledDiscoveryResourceTest.java new file mode 100644 index 00000000000..77607fc7005 --- /dev/null +++ b/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/LraDisabledDiscoveryResourceTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.testing.lra; + +import java.net.URI; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import io.helidon.lra.coordinator.Lra; +import io.helidon.microprofile.config.ConfigCdiExtension; +import io.helidon.microprofile.lra.LraCdiExtension; +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.AddExtension; +import io.helidon.microprofile.testing.junit5.AddJaxRs; +import io.helidon.microprofile.testing.junit5.DisableDiscovery; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.lra.annotation.Compensate; +import org.eclipse.microprofile.lra.annotation.Complete; +import org.eclipse.microprofile.lra.annotation.LRAStatus; +import org.eclipse.microprofile.lra.annotation.ws.rs.LRA; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@DisableDiscovery +@AddJaxRs +@AddBean(TestLraCoordinator.class) +@AddExtension(LraCdiExtension.class) +@AddExtension(ConfigCdiExtension.class) +@Path("/test/internal") +public class LraDisabledDiscoveryResourceTest { + + private final WebTarget target; + private final Set completedLras; + private final Set cancelledLras; + private final TestLraCoordinator coordinator; + + @Inject + public LraDisabledDiscoveryResourceTest(WebTarget target, + TestLraCoordinator coordinator) { + this.target = target; + this.coordinator = coordinator; + this.completedLras = new CopyOnWriteArraySet<>(); + this.cancelledLras = new CopyOnWriteArraySet<>(); + } + + @PUT + @Path("/withdraw") + @LRA(LRA.Type.REQUIRES_NEW) + public Response withdraw(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) Optional lraId, String content) { + if ("BOOM".equals(content)) { + throw new IllegalArgumentException("BOOM"); + } + return Response.ok().build(); + } + + @Complete + public void complete(URI lraId) { + completedLras.add(lraId.toString()); + } + + @Compensate + public void rollback(URI lraId) { + cancelledLras.add(lraId.toString()); + } + + @Test + public void testLraComplete() { + try (Response res = target + .path("/test/internal/withdraw") + .request() + .put(Entity.entity("test", MediaType.TEXT_PLAIN_TYPE))) { + assertThat(res.getStatus(), is(200)); + String lraId = res.getHeaderString(LRA.LRA_HTTP_CONTEXT_HEADER); + Lra lra = coordinator.lra(lraId); + assertThat(lra.status(), is(LRAStatus.Closed)); + assertThat(completedLras, contains(lraId)); + } + } + + @Test + public void testLraCompensate() { + try (Response res = target + .path("/test/internal/withdraw") + .request() + .put(Entity.entity("BOOM", MediaType.TEXT_PLAIN_TYPE))) { + assertThat(res.getStatus(), is(500)); + String lraId = res.getHeaderString(LRA.LRA_HTTP_CONTEXT_HEADER); + Lra lra = coordinator.lra(lraId); + assertThat(lra.status(), is(LRAStatus.Cancelled)); + assertThat(cancelledLras, contains(lraId)); + } + } +} diff --git a/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/LraExternalResourceTest.java b/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/LraExternalResourceTest.java new file mode 100644 index 00000000000..b2cf90842d6 --- /dev/null +++ b/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/LraExternalResourceTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.testing.lra; + +import io.helidon.lra.coordinator.Lra; +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.lra.annotation.LRAStatus; +import org.eclipse.microprofile.lra.annotation.ws.rs.LRA; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@AddBean(WithdrawTestResource.class) +@AddBean(TestLraCoordinator.class) +public class LraExternalResourceTest { + + private final WithdrawTestResource withdrawTestResource; + private final TestLraCoordinator coordinator; + private final WebTarget target; + + @Inject + public LraExternalResourceTest(WithdrawTestResource withdrawTestResource, TestLraCoordinator coordinator, WebTarget target) { + this.withdrawTestResource = withdrawTestResource; + this.coordinator = coordinator; + this.target = target; + } + + @Test + public void testLraComplete() { + try (Response res = target + .path("/test/external/withdraw") + .request() + .put(Entity.entity("test", MediaType.TEXT_PLAIN_TYPE))) { + assertThat(res.getStatus(), is(200)); + String lraId = res.getHeaderString(LRA.LRA_HTTP_CONTEXT_HEADER); + Lra lra = coordinator.lra(lraId); + assertThat(lra.status(), is(LRAStatus.Closed)); + assertThat(withdrawTestResource.getCompletedLras(), contains(lraId)); + } + } + + @Test + public void testLraCompensate() { + try (Response res = target + .path("/test/external/withdraw") + .request() + .put(Entity.entity("BOOM", MediaType.TEXT_PLAIN_TYPE))) { + assertThat(res.getStatus(), is(500)); + String lraId = res.getHeaderString(LRA.LRA_HTTP_CONTEXT_HEADER); + Lra lra = coordinator.lra(lraId); + assertThat(lra.status(), is(LRAStatus.Cancelled)); + assertThat(withdrawTestResource.getCancelledLras(), contains(lraId)); + } + } +} diff --git a/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/LraMultiPortTest.java b/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/LraMultiPortTest.java new file mode 100644 index 00000000000..a0487365e1a --- /dev/null +++ b/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/LraMultiPortTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.testing.lra; + +import java.net.URI; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import io.helidon.lra.coordinator.Lra; +import io.helidon.microprofile.server.RoutingName; +import io.helidon.microprofile.server.RoutingPath; +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.AddConfig; +import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.microprofile.testing.junit5.Socket; +import io.helidon.webserver.http.HttpService; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.lra.annotation.Compensate; +import org.eclipse.microprofile.lra.annotation.Complete; +import org.eclipse.microprofile.lra.annotation.LRAStatus; +import org.eclipse.microprofile.lra.annotation.ws.rs.LRA; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@AddConfig(key = "server.sockets.0.name", value = "test-route") +@AddConfig(key = "server.sockets.0.port", value = "0") +@AddConfig(key = "server.sockets.0.bind-address", value = "localhost") +@AddBean(TestLraCoordinator.class) +@Path("/test/multi-port") +public class LraMultiPortTest { + + private final WebTarget target; + private final WebTarget otherTarget; + private final Set completedLras; + private final Set cancelledLras; + private final TestLraCoordinator coordinator; + + @Inject + public LraMultiPortTest(WebTarget target, + TestLraCoordinator coordinator, + @Socket("test-route") WebTarget otherTarget) { + this.target = target; + this.coordinator = coordinator; + this.otherTarget = otherTarget; + this.completedLras = new CopyOnWriteArraySet<>(); + this.cancelledLras = new CopyOnWriteArraySet<>(); + } + + @Produces + @ApplicationScoped + @RoutingName("test-route") + @RoutingPath("/test/route") + public HttpService anotherRoute() { + return r -> r.any((req, res) -> res.send("Hello from test route!")); + } + + @PUT + @Path("/outer") + @LRA(LRA.Type.REQUIRES_NEW) + public Response withdraw(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) Optional lraId, String content) { + try (Response res = target.path("/test/multi-port/inner") + .request() + .put(Entity.entity(content, MediaType.TEXT_PLAIN_TYPE))) { + assertThat(res.getStatus(), is(Response.Status.OK.getStatusCode())); + } + return Response.ok().build(); + } + + @PUT + @Path("/inner") + @LRA(LRA.Type.REQUIRED) + public Response deposit(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) Optional lraId, String content) { + if ("BOOM".equals(content)) { + throw new IllegalArgumentException("BOOM"); + } + return Response.ok().build(); + } + + @Complete + public void complete(URI lraId) { + completedLras.add(lraId.toString()); + } + + @Compensate + public void rollback(URI lraId) { + cancelledLras.add(lraId.toString()); + } + + @Test + public void testLra() { + try (Response res = target + .path("/test/multi-port/outer") + .request() + .put(Entity.entity("test", MediaType.TEXT_PLAIN_TYPE))) { + assertThat(res.getStatus(), is(200)); + String lraId = res.getHeaderString(LRA.LRA_HTTP_CONTEXT_HEADER); + Lra lra = coordinator.lra(lraId); + assertThat(lra.status(), is(LRAStatus.Closed)); + assertThat(completedLras, contains(lraId)); + } + } + + @Test + public void testCompensatedLra() { + try (Response res = target + .path("/test/multi-port/outer") + .request() + .put(Entity.entity("BOOM", MediaType.TEXT_PLAIN_TYPE))) { + assertThat(res.getStatus(), is(500)); + String lraId = res.getHeaderString(LRA.LRA_HTTP_CONTEXT_HEADER); + Lra lra = coordinator.lra(lraId); + assertThat(lra.status(), is(LRAStatus.Cancelled)); + assertThat(cancelledLras, contains(lraId)); + } + } + + @Test + public void testOtherRoute() { + try (Response res = otherTarget + .path("/test/route") + .request() + .get()) { + assertThat(res.getStatus(), is(200)); + assertThat(res.readEntity(String.class), is("Hello from test route!")); + } + } + +} diff --git a/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/NonLraResourceTest.java b/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/NonLraResourceTest.java new file mode 100644 index 00000000000..774541b117c --- /dev/null +++ b/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/NonLraResourceTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.testing.lra; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@Path("/test/non-lra") +public class NonLraResourceTest { + + private final WebTarget target; + + @Inject + public NonLraResourceTest(WebTarget target) { + this.target = target; + } + + @GET + @Path("/say-hi") + public String sayHi() { + return "Hi!"; + } + + @Test + public void testNonLraResource() { + try (Response res = target + .path("/test/non-lra/say-hi") + .request() + .get()) { + assertThat(res.getStatus(), is(200)); + assertThat(res.readEntity(String.class), is("Hi!")); + } + } +} diff --git a/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/WithdrawTestResource.java b/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/WithdrawTestResource.java new file mode 100644 index 00000000000..9487bc54b50 --- /dev/null +++ b/microprofile/lra/testing/src/test/java/io/helidon/microprofile/testing/lra/WithdrawTestResource.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.testing.lra; + +import java.net.URI; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.lra.annotation.Compensate; +import org.eclipse.microprofile.lra.annotation.Complete; +import org.eclipse.microprofile.lra.annotation.ws.rs.LRA; + +@ApplicationScoped +@Path("/test/external") +public class WithdrawTestResource { + + private final Set completedLras; + private final Set cancelledLras; + + public WithdrawTestResource() { + this.completedLras = new CopyOnWriteArraySet<>(); + this.cancelledLras = new CopyOnWriteArraySet<>(); + } + + @PUT + @Path("/withdraw") + @LRA(LRA.Type.REQUIRES_NEW) + public Response withdraw(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) Optional lraId, String content) { + if ("BOOM".equals(content)) { + throw new IllegalArgumentException("BOOM"); + } + return Response.ok().build(); + } + + @Complete + public void complete(URI lraId) { + completedLras.add(lraId.toString()); + } + + @Compensate + public void rollback(URI lraId) { + cancelledLras.add(lraId.toString()); + } + + Set getCompletedLras() { + return completedLras; + } + + Set getCancelledLras() { + return cancelledLras; + } +} diff --git a/microprofile/lra/testing/src/test/resources/logging-test.properties b/microprofile/lra/testing/src/test/resources/logging-test.properties new file mode 100644 index 00000000000..a61f39ea0cb --- /dev/null +++ b/microprofile/lra/testing/src/test/resources/logging-test.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=WARNING + +io.helidon.level=INFO diff --git a/microprofile/messaging/core/pom.xml b/microprofile/messaging/core/pom.xml index eb8a1d95b98..95d03e55c74 100644 --- a/microprofile/messaging/core/pom.xml +++ b/microprofile/messaging/core/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.messaging helidon-microprofile-messaging-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-messaging diff --git a/microprofile/messaging/health/pom.xml b/microprofile/messaging/health/pom.xml index db4a7fd548b..9da91150737 100644 --- a/microprofile/messaging/health/pom.xml +++ b/microprofile/messaging/health/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.messaging helidon-microprofile-messaging-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-messaging-health diff --git a/microprofile/messaging/metrics/pom.xml b/microprofile/messaging/metrics/pom.xml index 2fc436f3ddf..e07edf4f76c 100644 --- a/microprofile/messaging/metrics/pom.xml +++ b/microprofile/messaging/metrics/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.messaging helidon-microprofile-messaging-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-messaging-metrics diff --git a/microprofile/messaging/pom.xml b/microprofile/messaging/pom.xml index 9459a75c439..006d4294f77 100644 --- a/microprofile/messaging/pom.xml +++ b/microprofile/messaging/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.messaging diff --git a/microprofile/metrics/pom.xml b/microprofile/metrics/pom.xml index 8a629850243..aba57224710 100644 --- a/microprofile/metrics/pom.xml +++ b/microprofile/metrics/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.metrics helidon-microprofile-metrics diff --git a/microprofile/oidc/pom.xml b/microprofile/oidc/pom.xml index 8b498d5aa64..d60405b0d32 100644 --- a/microprofile/oidc/pom.xml +++ b/microprofile/oidc/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-oidc Helidon Microprofile Security OIDC Integration diff --git a/microprofile/openapi/pom.xml b/microprofile/openapi/pom.xml index 0adf9f47f46..78bc463c743 100644 --- a/microprofile/openapi/pom.xml +++ b/microprofile/openapi/pom.xml @@ -21,7 +21,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.openapi helidon-microprofile-openapi diff --git a/microprofile/pom.xml b/microprofile/pom.xml index b1e99f51df2..5f157a3c6d8 100644 --- a/microprofile/pom.xml +++ b/microprofile/pom.xml @@ -24,7 +24,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.microprofile diff --git a/microprofile/reactive-streams/pom.xml b/microprofile/reactive-streams/pom.xml index 8532a8f57ce..68bd8493505 100644 --- a/microprofile/reactive-streams/pom.xml +++ b/microprofile/reactive-streams/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.reactive-streams diff --git a/microprofile/rest-client/pom.xml b/microprofile/rest-client/pom.xml index af4c65e87eb..a0a9ffbe1ef 100644 --- a/microprofile/rest-client/pom.xml +++ b/microprofile/rest-client/pom.xml @@ -23,7 +23,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.rest-client diff --git a/microprofile/scheduling/pom.xml b/microprofile/scheduling/pom.xml index 875690936bc..1b9d5470bc8 100644 --- a/microprofile/scheduling/pom.xml +++ b/microprofile/scheduling/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.scheduling diff --git a/microprofile/security/pom.xml b/microprofile/security/pom.xml index ead9cde3743..4c7c4e2c16e 100644 --- a/microprofile/security/pom.xml +++ b/microprofile/security/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-security Helidon Microprofile Security Integration diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/JerseySecurityContext.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/JerseySecurityContext.java index 76bd0453d9e..479d3e782df 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/JerseySecurityContext.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/JerseySecurityContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ public Principal getUserPrincipal() { @Override public boolean isUserInRole(String role) { - return securityContext.isUserInRole(role, methodSecurity.getAuthorizer()); + return securityContext.isUserInRole(role, methodSecurity.authorizer()); } @Override diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityDefinition.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityDefinition.java index 4e3df064f93..53624d5f9ab 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityDefinition.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityDefinition.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; +import io.helidon.common.config.Config; import io.helidon.security.AuditEvent; import io.helidon.security.SecurityLevel; import io.helidon.security.annotations.Audited; @@ -97,7 +98,21 @@ SecurityDefinition copyMe() { return result; } - public void add(Authenticated atn) { + void fromConfig(Config config) { + config.get("authorize").as(Boolean.class).ifPresent(this::requiresAuthorization); + config.get("authorizer").as(String.class).ifPresent(this::authorizer); + config.get("authorization-explicit").as(Boolean.class).ifPresent(this::atzExplicit); + config.get("authenticate").as(Boolean.class).ifPresent(this::requiresAuthentication); + config.get("authenticator").as(String.class).ifPresent(this::authenticator); + config.get("authentication-optional").as(Boolean.class).ifPresent(this::authenticationOptional); + config.get("audit").as(Boolean.class).ifPresent(this::audited); + config.get("audit-event-type").as(String.class).ifPresent(this::auditEventType); + config.get("audit-message-format").as(String.class).ifPresent(this::auditMessageFormat); + config.get("audit-ok-severity").as(AuditEvent.AuditSeverity.class).ifPresent(this::auditOkSeverity); + config.get("audit-error-severity").as(AuditEvent.AuditSeverity.class).ifPresent(this::auditErrorSeverity); + } + + void add(Authenticated atn) { if (null == atn) { return; } @@ -106,7 +121,7 @@ public void add(Authenticated atn) { this.authenticator = "".equals(atn.provider()) ? null : atn.provider(); } - public void add(Authorized atz) { + void add(Authorized atz) { if (null == atz) { return; } @@ -130,7 +145,7 @@ void requiresAuthentication(boolean atn) { this.requiresAuthentication = atn; } - void setRequiresAuthorization(boolean atz) { + void requiresAuthorization(boolean atz) { this.requiresAuthorization = atz; } @@ -154,10 +169,18 @@ boolean authenticationOptional() { return authnOptional; } + void authenticationOptional(boolean authnOptional) { + this.authnOptional = authnOptional; + } + boolean failOnFailureIfOptional() { return failOnFailureIfOptional; } + void failOnFailureIfOptional(boolean failOnFailureIfOptional) { + this.failOnFailureIfOptional = failOnFailureIfOptional; + } + boolean requiresAuthorization() { if (null != requiresAuthorization) { return requiresAuthorization; @@ -171,47 +194,79 @@ boolean requiresAuthorization() { return (count != 0) || authorizeByDefault; } - public boolean isAtzExplicit() { + boolean atzExplicit() { return atzExplicit; } - String getAuthenticator() { + void atzExplicit(boolean atzExplicit) { + this.atzExplicit = atzExplicit; + } + + String authenticator() { return authenticator; } - String getAuthorizer() { + void authenticator(String authenticator) { + this.authenticator = authenticator; + } + + String authorizer() { return authorizer; } - public List getSecurityLevels() { + void authorizer(String authorizer) { + this.authorizer = authorizer; + } + + List securityLevels() { return securityLevels; } - public boolean isAudited() { + boolean audited() { return audited; } - public String getAuditEventType() { + void audited(boolean audited) { + this.audited = audited; + } + + String auditEventType() { return auditEventType; } - public String getAuditMessageFormat() { + void auditEventType(String auditEventType) { + this.auditEventType = auditEventType; + } + + String auditMessageFormat() { return auditMessageFormat; } - public AuditEvent.AuditSeverity getAuditOkSeverity() { + void auditMessageFormat(String auditMessageFormat) { + this.auditMessageFormat = auditMessageFormat; + } + + AuditEvent.AuditSeverity auditOkSeverity() { return auditOkSeverity; } - public AuditEvent.AuditSeverity getAuditErrorSeverity() { + void auditOkSeverity(AuditEvent.AuditSeverity auditOkSeverity) { + this.auditOkSeverity = auditOkSeverity; + } + + AuditEvent.AuditSeverity auditErrorSeverity() { return auditErrorSeverity; } - public AnnotationAnalyzer.AnalyzerResponse analyzerResponse(AnnotationAnalyzer analyzer) { + void auditErrorSeverity(AuditEvent.AuditSeverity auditOkSeverity) { + this.auditErrorSeverity = auditOkSeverity; + } + + AnnotationAnalyzer.AnalyzerResponse analyzerResponse(AnnotationAnalyzer analyzer) { return analyzerResponses.get(analyzer); } - public void analyzerResponse(AnnotationAnalyzer analyzer, AnnotationAnalyzer.AnalyzerResponse analyzerResponse) { + void analyzerResponse(AnnotationAnalyzer analyzer, AnnotationAnalyzer.AnalyzerResponse analyzerResponse) { analyzerResponses.put(analyzer, analyzerResponse); switch (analyzerResponse.authenticationResponse()) { diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java index 8fc0fc23e98..101a640e81f 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilter.java @@ -31,6 +31,7 @@ import io.helidon.common.HelidonServiceLoader; import io.helidon.common.config.Config; import io.helidon.common.context.Contexts; +import io.helidon.common.uri.UriPath; import io.helidon.jersey.common.InvokedResource; import io.helidon.security.AuditEvent; import io.helidon.security.Security; @@ -155,9 +156,9 @@ protected void processSecurity(ContainerRequestContext request, * Authentication */ authenticate(filterContext, securityContext, tracing.atnTracing()); - LOGGER.log(Level.TRACE, () -> "Filter after authentication. Should finish: " + filterContext.isShouldFinish()); + LOGGER.log(Level.TRACE, () -> "Filter after authentication. Should finish: " + filterContext.shouldFinish()); // authentication failed - if (filterContext.isShouldFinish()) { + if (filterContext.shouldFinish()) { return; } @@ -203,7 +204,7 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont SecurityDefinition methodSecurity = jerseySecurityContext.methodSecurity(); SecurityContext securityContext = jerseySecurityContext.securityContext(); - if (fc.isExplicitAtz() && !securityContext.isAuthorized()) { + if (fc.explicitAtz() && !securityContext.isAuthorized()) { // now we have an option that the response code is already an error (e.g. BadRequest) // in such a case we return the original error, as we may have never reached the method code switch (responseContext.getStatusInfo().getFamily()) { @@ -223,11 +224,11 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont responseContext.setEntity(""); } responseContext.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); - LOGGER.log(Level.ERROR, "Authorization failure. Request for" + fc.getResourcePath() + LOGGER.log(Level.ERROR, "Authorization failure. Request for" + fc.resourcePath() + " has failed, as it was marked" + "as explicitly authorized, yet authorization was never called on security context. The " + "method was invoked and may have changed data. Marking as internal server error"); - fc.setShouldFinish(true); + fc.shouldFinish(true); break; } } @@ -235,26 +236,26 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont ResponseTracing responseTracing = SecurityTracing.get().responseTracing(); try { - if (methodSecurity.isAudited()) { + if (methodSecurity.audited()) { AuditEvent.AuditSeverity auditSeverity; if (responseContext.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) { - auditSeverity = methodSecurity.getAuditOkSeverity(); + auditSeverity = methodSecurity.auditOkSeverity(); } else { - auditSeverity = methodSecurity.getAuditErrorSeverity(); + auditSeverity = methodSecurity.auditErrorSeverity(); } SecurityAuditEvent auditEvent = SecurityAuditEvent - .audit(auditSeverity, methodSecurity.getAuditEventType(), methodSecurity.getAuditMessageFormat()) - .addParam(AuditEvent.AuditParam.plain("method", fc.getMethod())) - .addParam(AuditEvent.AuditParam.plain("path", fc.getResourcePath())) + .audit(auditSeverity, methodSecurity.auditEventType(), methodSecurity.auditMessageFormat()) + .addParam(AuditEvent.AuditParam.plain("method", fc.method())) + .addParam(AuditEvent.AuditParam.plain("path", fc.resourcePath())) .addParam(AuditEvent.AuditParam.plain("status", String.valueOf(responseContext.getStatus()))) .addParam(AuditEvent.AuditParam.plain("subject", securityContext.user() .or(securityContext::service) .orElse(SecurityContext.ANONYMOUS))) .addParam(AuditEvent.AuditParam.plain("transport", "http")) - .addParam(AuditEvent.AuditParam.plain("resourceType", fc.getResourceName())) - .addParam(AuditEvent.AuditParam.plain("targetUri", fc.getTargetUri())); + .addParam(AuditEvent.AuditParam.plain("resourceType", fc.resourceName())) + .addParam(AuditEvent.AuditParam.plain("targetUri", fc.targetUri())); securityContext.audit(auditEvent); } @@ -271,16 +272,16 @@ protected SecurityFilterContext initRequestFiltering(ContainerRequestContext req return invokedResource .definitionMethod() .map(definitionMethod -> { - context.setMethodSecurity(getMethodSecurity(invokedResource, - definitionMethod, - (ExtendedUriInfo) requestContext.getUriInfo())); - context.setResourceName(definitionMethod.getDeclaringClass().getSimpleName()); + context.methodSecurity(getMethodSecurity(invokedResource, + definitionMethod, + (ExtendedUriInfo) requestContext.getUriInfo())); + context.resourceName(definitionMethod.getDeclaringClass().getSimpleName()); return configureContext(context, requestContext, requestContext.getUriInfo()); }) .orElseGet(() -> { // this will end in 404, just let it on - context.setShouldFinish(true); + context.shouldFinish(true); return context; }); } @@ -325,7 +326,7 @@ private SecurityDefinition securityForClass(Class theClass, SecurityDefinitio SecurityLevel securityLevel = SecurityLevel.create(realClass.getName()) .withClassAnnotations(customAnnotsMap) .build(); - definition.getSecurityLevels().add(securityLevel); + definition.securityLevels().add(securityLevel); for (AnnotationAnalyzer analyzer : analyzers) { AnnotationAnalyzer.AnalyzerResponse analyzerResponse; @@ -344,20 +345,6 @@ private SecurityDefinition securityForClass(Class theClass, SecurityDefinitio return definition; } - /** - * Returns the real class of this object, skipping proxies. - * - * @param object The object. - * @return Its class. - */ - private static Class getRealClass(Class object) { - Class result = object; - while (result.isSynthetic()) { - result = result.getSuperclass(); - } - return result; - } - private SecurityDefinition getMethodSecurity(InvokedResource invokedResource, Method definitionMethod, ExtendedUriInfo uriInfo) { @@ -427,9 +414,9 @@ private SecurityDefinition getMethodSecurity(InvokedResource invokedResource, for (Method method : methodsToProcess) { Class clazz = method.getDeclaringClass(); current = securityForClass(clazz, current); - SecurityDefinition methodDef = processMethod(current.copyMe(), method); + SecurityDefinition methodDef = processMethod(current.copyMe(), uriInfo.getPath(), method); - SecurityLevel currentSecurityLevel = methodDef.getSecurityLevels().get(methodDef.getSecurityLevels().size() - 1); + SecurityLevel currentSecurityLevel = methodDef.securityLevels().get(methodDef.securityLevels().size() - 1); Map, List> methodAnnotations = new HashMap<>(); addCustomAnnotations(methodAnnotations, method); @@ -437,7 +424,7 @@ private SecurityDefinition getMethodSecurity(InvokedResource invokedResource, .withMethodName(method.getName()) .withMethodAnnotations(methodAnnotations) .build(); - methodDef.getSecurityLevels().set(methodDef.getSecurityLevels().size() - 1, newSecurityLevel); + methodDef.securityLevels().set(methodDef.securityLevels().size() - 1, newSecurityLevel); for (AnnotationAnalyzer analyzer : analyzers) { AnnotationAnalyzer.AnalyzerResponse analyzerResponse = analyzer.analyze(method, current.analyzerResponse(analyzer)); @@ -466,16 +453,14 @@ private SecurityDefinition getMethodSecurity(InvokedResource invokedResource, } SecurityDefinition resClassSecurity = obtainClassSecurityDefinition(appRealClass, appClassSecurity, definitionClass); + SecurityDefinition methodDef = processMethod(resClassSecurity, uriInfo.getRequestUri().getPath(), definitionMethod); - - SecurityDefinition methodDef = processMethod(resClassSecurity, definitionMethod); - - int index = methodDef.getSecurityLevels().size() - 1; - SecurityLevel currentSecurityLevel = methodDef.getSecurityLevels().get(index); + int index = methodDef.securityLevels().size() - 1; + SecurityLevel currentSecurityLevel = methodDef.securityLevels().get(index); Map, List> methodLevelAnnotations = new HashMap<>(); addCustomAnnotations(methodLevelAnnotations, definitionMethod); - methodDef.getSecurityLevels().set(index, SecurityLevel.create(currentSecurityLevel) + methodDef.securityLevels().set(index, SecurityLevel.create(currentSecurityLevel) .withMethodName(definitionMethod.getName()) .withMethodAnnotations(methodLevelAnnotations) .build()); @@ -533,14 +518,19 @@ List analyzers() { return this.analyzers; } - private static SecurityDefinition processMethod(SecurityDefinition current, Method method) { - Authenticated atn = method.getAnnotation(Authenticated.class); - Authorized atz = method.getAnnotation(Authorized.class); - Audited audited = method.getAnnotation(Audited.class); + private SecurityDefinition processMethod(SecurityDefinition current, String path, Method method) { SecurityDefinition methodDef = current.copyMe(); - methodDef.add(atn); - methodDef.add(atz); - methodDef.add(audited); + findMethodConfig(UriPath.create(path)) + .asNode() + .ifPresentOrElse(methodDef::fromConfig, + () -> { + Authenticated atn = method.getAnnotation(Authenticated.class); + Authorized atz = method.getAnnotation(Authorized.class); + Audited audited = method.getAnnotation(Audited.class); + methodDef.add(atn); + methodDef.add(atz); + methodDef.add(audited); + }); return methodDef; } diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java index a2b7578b5b8..f37a97024ff 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterCommon.java @@ -23,9 +23,14 @@ import java.util.ServiceLoader; import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; import io.helidon.common.config.Config; import io.helidon.common.context.Contexts; +import io.helidon.common.uri.UriPath; import io.helidon.common.uri.UriQuery; +import io.helidon.config.mp.MpConfig; +import io.helidon.http.PathMatcher; +import io.helidon.http.PathMatchers; import io.helidon.microprofile.security.spi.SecurityResponseMapper; import io.helidon.security.AuthenticationResponse; import io.helidon.security.AuthorizationResponse; @@ -45,6 +50,7 @@ import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; +import org.eclipse.microprofile.config.ConfigProvider; import org.glassfish.jersey.server.ContainerRequest; /** @@ -55,6 +61,7 @@ abstract class SecurityFilterCommon { private static final List RESPONSE_MAPPERS = HelidonServiceLoader .builder(ServiceLoader.load(SecurityResponseMapper.class)).build().asList(); + private static final LazyValue> PATH_CONFIGS = LazyValue.create(SecurityFilterCommon::createPathConfigs); private final Security security; @@ -67,6 +74,30 @@ abstract class SecurityFilterCommon { this.featureConfig = featureConfig; } + private static List createPathConfigs() { + return MpConfig.toHelidonConfig(ConfigProvider.getConfig()) + .get("server.features.security.endpoints") + .asNodeList() + .orElse(List.of()) + .stream() + .map(PathConfig::create) + .toList(); + } + + /** + * Returns the real class of this object, skipping proxies. + * + * @param object The object. + * @return Its class. + */ + static Class getRealClass(Class object) { + Class result = object; + while (result.isSynthetic()) { + result = result.getSuperclass(); + } + return result; + } + protected void doFilter(ContainerRequestContext request, SecurityContext securityContext) { SecurityTracing tracing = SecurityTracing.get(); tracing.securityContext(securityContext); @@ -79,7 +110,7 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit filterContext); } - if (filterContext.isShouldFinish()) { + if (filterContext.shouldFinish()) { if (logger().isLoggable(Level.TRACE)) { logger().log(Level.TRACE, "Endpoint %s not found, no security", request.getUriInfo().getRequestUri()); } @@ -96,17 +127,17 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit } else { origRequest = requestUri.getPath() + "?" + query; } - Map> allHeaders = new HashMap<>(filterContext.getHeaders()); + Map> allHeaders = new HashMap<>(filterContext.headers()); allHeaders.put(Security.HEADER_ORIG_URI, List.of(origRequest)); SecurityEnvironment.Builder envBuilder = SecurityEnvironment.builder(security.serverTime()) .transport(requestUri.getScheme()) - .path(filterContext.getResourcePath()) - .targetUri(filterContext.getTargetUri()) - .method(filterContext.getMethod()) - .queryParams(filterContext.getQueryParams()) + .path(filterContext.resourcePath()) + .targetUri(filterContext.targetUri()) + .method(filterContext.method()) + .queryParams(filterContext.queryParams()) .headers(allHeaders) - .addAttribute("resourceType", filterContext.getResourceName()); + .addAttribute("resourceType", filterContext.resourceName()); // The following two lines are not possible in JAX-RS or Jersey - we would have to touch // underlying web server's request... @@ -120,9 +151,14 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit } SecurityEnvironment env = envBuilder.build(); + Map configMap = new HashMap<>(); + findMethodConfig(UriPath.create(requestUri.getPath())) + .asNode() + .ifPresent(conf -> conf.asNodeList().get().forEach(node -> configMap.put(node.name(), node))); EndpointConfig ec = EndpointConfig.builder() - .securityLevels(filterContext.getMethodSecurity().getSecurityLevels()) + .securityLevels(filterContext.methodSecurity().securityLevels()) + .configMap(configMap) .build(); try { @@ -132,12 +168,12 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit request.setProperty(PROP_FILTER_CONTEXT, filterContext); //context is needed even if authn/authz fails - for auditing request.setSecurityContext(new JerseySecurityContext(securityContext, - filterContext.getMethodSecurity(), - "https".equals(filterContext.getTargetUri().getScheme()))); + filterContext.methodSecurity(), + "https".equals(filterContext.targetUri().getScheme()))); processSecurity(request, filterContext, tracing, securityContext); } finally { - if (filterContext.isTraceSuccess()) { + if (filterContext.traceSuccess()) { tracing.logProceed(); tracing.finish(); } else { @@ -147,13 +183,22 @@ protected void doFilter(ContainerRequestContext request, SecurityContext securit } } + Config findMethodConfig(UriPath path) { + return PATH_CONFIGS.get() + .stream() + .filter(pathConfig -> pathConfig.pathMatcher.prefixMatch(path).accepted()) + .findFirst() + .map(PathConfig::config) + .orElseGet(Config::empty); + } + protected void authenticate(SecurityFilterContext context, SecurityContext securityContext, AtnTracing atnTracing) { try { - SecurityDefinition methodSecurity = context.getMethodSecurity(); + SecurityDefinition methodSecurity = context.methodSecurity(); if (methodSecurity.requiresAuthentication()) { if (logger().isLoggable(Level.TRACE)) { - logger().log(Level.TRACE, "Endpoint {0} requires authentication", context.getTargetUri()); + logger().log(Level.TRACE, "Endpoint {0} requires authentication", context.targetUri()); } //authenticate request SecurityClientBuilder clientBuilder = securityContext @@ -161,15 +206,15 @@ protected void authenticate(SecurityFilterContext context, SecurityContext secur .optional(methodSecurity.authenticationOptional()) .tracingSpan(atnTracing.findParent().orElse(null)); - clientBuilder.explicitProvider(methodSecurity.getAuthenticator()); + clientBuilder.explicitProvider(methodSecurity.authenticator()); processAuthentication(context, clientBuilder, methodSecurity, atnTracing); } else { if (logger().isLoggable(Level.TRACE)) { - logger().log(Level.TRACE, "Endpoint {0} does not require authentication", context.getTargetUri()); + logger().log(Level.TRACE, "Endpoint {0} does not require authentication", context.targetUri()); } } } finally { - if (context.isTraceSuccess()) { + if (context.traceSuccess()) { securityContext.user() .ifPresent(atnTracing::logUser); @@ -178,9 +223,9 @@ protected void authenticate(SecurityFilterContext context, SecurityContext secur atnTracing.finish(); } else { - Throwable ctxThrowable = context.getTraceThrowable(); + Throwable ctxThrowable = context.traceThrowable(); if (null == ctxThrowable) { - atnTracing.error(context.getTraceDescription()); + atnTracing.error(context.traceDescription()); } else { atnTracing.error(ctxThrowable); } @@ -209,17 +254,17 @@ protected void processAuthentication(SecurityFilterContext context, if (methodSecurity.authenticationOptional()) { logger().log(Level.TRACE, "Authentication failed, but was optional, so assuming anonymous"); } else { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setTraceThrowable(response.throwable().orElse(null)); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.traceThrowable(response.throwable().orElse(null)); + context.shouldFinish(true); int status = response.statusCode().orElse(Response.Status.UNAUTHORIZED.getStatusCode()); abortRequest(context, response, status, Map.of()); } } case SUCCESS_FINISH -> { - context.setShouldFinish(true); + context.shouldFinish(true); int status = response.statusCode().orElse(Response.Status.OK.getStatusCode()); abortRequest(context, response, status, Map.of()); } @@ -227,9 +272,9 @@ protected void processAuthentication(SecurityFilterContext context, if (methodSecurity.authenticationOptional()) { logger().log(Level.TRACE, "Authentication failed, but was optional, so assuming anonymous"); } else { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.shouldFinish(true); abortRequest(context, response, Response.Status.UNAUTHORIZED.getStatusCode(), @@ -240,23 +285,23 @@ protected void processAuthentication(SecurityFilterContext context, if (methodSecurity.authenticationOptional() && !methodSecurity.failOnFailureIfOptional()) { logger().log(Level.TRACE, "Authentication failed, but was optional, so assuming anonymous"); } else { - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setTraceThrowable(response.throwable().orElse(null)); - context.setTraceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.traceThrowable(response.throwable().orElse(null)); + context.traceSuccess(false); abortRequest(context, response, Response.Status.UNAUTHORIZED.getStatusCode(), Map.of()); - context.setShouldFinish(true); + context.shouldFinish(true); } } //noinspection DuplicatedCode default -> { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse("UNKNOWN_RESPONSE: " + responseStatus)); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse("UNKNOWN_RESPONSE: " + responseStatus)); + context.shouldFinish(true); SecurityException throwable = new SecurityException("Invalid SecurityStatus returned: " + responseStatus); - context.setTraceThrowable(throwable); + context.traceThrowable(throwable); throw throwable; } } @@ -267,41 +312,41 @@ protected void processAuthentication(SecurityFilterContext context, protected void authorize(SecurityFilterContext context, SecurityContext securityContext, AtzTracing atzTracing) { - if (context.getMethodSecurity().isAtzExplicit()) { + if (context.methodSecurity().atzExplicit()) { // authorization is explicitly done by user, we MUST skip it here if (logger().isLoggable(Level.TRACE)) { - logger().log(Level.TRACE, "Endpoint {0} uses explicit authorization, skipping", context.getTargetUri()); + logger().log(Level.TRACE, "Endpoint {0} uses explicit authorization, skipping", context.targetUri()); } - context.setExplicitAtz(true); + context.explicitAtz(true); return; } try { //now authorize (also authorize anonymous requests, as we may have a path-based authorization that allows public // access - if (context.getMethodSecurity().requiresAuthorization()) { + if (context.methodSecurity().requiresAuthorization()) { if (logger().isLoggable(Level.TRACE)) { - logger().log(Level.TRACE, "Endpoint {0} requires authorization", context.getTargetUri()); + logger().log(Level.TRACE, "Endpoint {0} requires authorization", context.targetUri()); } SecurityClientBuilder clientBuilder = securityContext.atzClientBuilder() .tracingSpan(atzTracing.findParent().orElse(null)) - .explicitProvider(context.getMethodSecurity().getAuthorizer()); + .explicitProvider(context.methodSecurity().authorizer()); processAuthorization(context, clientBuilder); } else { if (logger().isLoggable(Level.TRACE)) { logger().log(Level.TRACE, "Endpoint {0} does not require authorization. Method security: {1}", - context.getTargetUri(), - context.getMethodSecurity()); + context.targetUri(), + context.methodSecurity()); } } } finally { - if (context.isTraceSuccess()) { + if (context.traceSuccess()) { atzTracing.finish(); } else { - Throwable throwable = context.getTraceThrowable(); + Throwable throwable = context.traceThrowable(); if (null == throwable) { - atzTracing.error(context.getTraceDescription()); + atzTracing.error(context.traceDescription()); } else { atzTracing.error(throwable); } @@ -321,32 +366,32 @@ protected void processAuthorization(SecurityFilterContext context, //everything is fine, we can continue with processing } case FAILURE_FINISH -> { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setTraceThrowable(response.throwable().orElse(null)); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.traceThrowable(response.throwable().orElse(null)); + context.shouldFinish(true); int status = response.statusCode().orElse(Response.Status.FORBIDDEN.getStatusCode()); abortRequest(context, response, status, Map.of()); } case SUCCESS_FINISH -> { - context.setShouldFinish(true); + context.shouldFinish(true); int status = response.statusCode().orElse(Response.Status.OK.getStatusCode()); abortRequest(context, response, status, Map.of()); } case FAILURE -> { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setTraceThrowable(response.throwable().orElse(null)); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.traceThrowable(response.throwable().orElse(null)); + context.shouldFinish(true); abortRequest(context, response, response.statusCode().orElse(Response.Status.FORBIDDEN.getStatusCode()), Map.of()); } case ABSTAIN -> { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse(responseStatus.toString())); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse(responseStatus.toString())); + context.shouldFinish(true); abortRequest(context, response, response.statusCode().orElse(Response.Status.FORBIDDEN.getStatusCode()), @@ -354,11 +399,11 @@ protected void processAuthorization(SecurityFilterContext context, } //noinspection DuplicatedCode default -> { - context.setTraceSuccess(false); - context.setTraceDescription(response.description().orElse("UNKNOWN_RESPONSE: " + responseStatus)); - context.setShouldFinish(true); + context.traceSuccess(false); + context.traceDescription(response.description().orElse("UNKNOWN_RESPONSE: " + responseStatus)); + context.shouldFinish(true); SecurityException throwable = new SecurityException("Invalid SecurityStatus returned: " + responseStatus); - context.setTraceThrowable(throwable); + context.traceThrowable(throwable); throw throwable; } } @@ -389,7 +434,7 @@ protected void abortRequest(SecurityFilterContext context, } if (featureConfig.useAbortWith()) { - context.getJerseyRequest().abortWith(responseBuilder.build()); + context.jerseyRequest().abortWith(responseBuilder.build()); } else { String description = response.description() .orElse("Security did not allow this request to proceed."); @@ -408,17 +453,17 @@ protected void updateHeaders(Map> responseHeaders, Response protected SecurityFilterContext configureContext(SecurityFilterContext context, ContainerRequestContext requestContext, UriInfo uriInfo) { - context.setMethod(requestContext.getMethod()); - context.setHeaders(requestContext.getHeaders()); - context.setTargetUri(requestContext.getUriInfo().getRequestUri()); - context.setResourcePath(context.getTargetUri().getPath()); - context.setQueryParams(UriQuery.create(uriInfo.getRequestUri())); + context.method(requestContext.getMethod()); + context.headers(requestContext.getHeaders()); + context.targetUri(requestContext.getUriInfo().getRequestUri()); + context.resourcePath(context.targetUri().getPath()); + context.queryParams(UriQuery.create(uriInfo.getRequestUri())); - context.setJerseyRequest((ContainerRequest) requestContext); + context.jerseyRequest((ContainerRequest) requestContext); // now extract headers featureConfig().getQueryParamHandlers() - .forEach(handler -> handler.extract(uriInfo, context.getHeaders())); + .forEach(handler -> handler.extract(uriInfo, context.headers())); return context; } @@ -441,4 +486,14 @@ protected abstract void processSecurity(ContainerRequestContext request, Config config(String child) { return security.configFor(child); } + + private record PathConfig(PathMatcher pathMatcher, Config config) { + + static PathConfig create(Config config) { + String path = config.get("path").asString().orElseThrow(); + PathMatcher matcher = PathMatchers.create(path); + return new PathConfig(matcher, config.get("config")); + } + + } } diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterContext.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterContext.java index 20d082c48ba..946308d8390 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterContext.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityFilterContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,9 @@ */ public class SecurityFilterContext { private String resourceName; + private String fullResourceName; + private String resourceMethod; + private String fullResourceMethod; private String resourcePath; private String method; private Map> headers; @@ -60,113 +63,137 @@ public String toString() { + '}'; } - String getResourceName() { + String resourceName() { return resourceName; } - void setResourceName(String resourceName) { + void resourceName(String resourceName) { this.resourceName = resourceName; } - String getResourcePath() { + String fullResourceName() { + return fullResourceName; + } + + void fullResourceName(String fullResourceName) { + this.fullResourceName = fullResourceName; + } + + String resourceMethod() { + return resourceMethod; + } + + void resourceMethod(String resourceMethod) { + this.resourceMethod = resourceMethod; + } + + String fullResourceMethod() { + return fullResourceMethod; + } + + void fullResourceMethod(String fullResourceMethod) { + this.fullResourceMethod = fullResourceMethod; + } + + String resourcePath() { return resourcePath; } - void setResourcePath(String resourcePath) { + void resourcePath(String resourcePath) { this.resourcePath = resourcePath; } - String getMethod() { + String method() { return method; } - void setMethod(String method) { + void method(String method) { this.method = method; } - Map> getHeaders() { + Map> headers() { return headers; } - void setHeaders(Map> headers) { + void headers(Map> headers) { this.headers = headers; } - URI getTargetUri() { + URI targetUri() { return targetUri; } - void setTargetUri(URI targetUri) { + void targetUri(URI targetUri) { this.targetUri = targetUri; } - ContainerRequest getJerseyRequest() { + ContainerRequest jerseyRequest() { return jerseyRequest; } - void setJerseyRequest(ContainerRequest jerseyRequest) { + void jerseyRequest(ContainerRequest jerseyRequest) { this.jerseyRequest = jerseyRequest; } - boolean isShouldFinish() { + boolean shouldFinish() { return shouldFinish; } - void setShouldFinish(boolean shouldFinish) { + void shouldFinish(boolean shouldFinish) { this.shouldFinish = shouldFinish; } - SecurityDefinition getMethodSecurity() { + SecurityDefinition methodSecurity() { return methodSecurity; } - void setMethodSecurity(SecurityDefinition methodSecurity) { + void methodSecurity(SecurityDefinition methodSecurity) { this.methodSecurity = methodSecurity; } - boolean isExplicitAtz() { + boolean explicitAtz() { return explicitAtz; } - void setExplicitAtz(boolean explicitAtz) { + void explicitAtz(boolean explicitAtz) { this.explicitAtz = explicitAtz; } - boolean isTraceSuccess() { + boolean traceSuccess() { return traceSuccess; } - void setTraceSuccess(boolean traceSuccess) { + void traceSuccess(boolean traceSuccess) { this.traceSuccess = traceSuccess; } - String getTraceDescription() { + String traceDescription() { return traceDescription; } - void setTraceDescription(String traceDescription) { + void traceDescription(String traceDescription) { this.traceDescription = traceDescription; } - Throwable getTraceThrowable() { + Throwable traceThrowable() { return traceThrowable; } - void setTraceThrowable(Throwable traceThrowable) { + void traceThrowable(Throwable traceThrowable) { this.traceThrowable = traceThrowable; } - UriQuery getQueryParams() { + UriQuery queryParams() { return queryParams; } - void setQueryParams(UriQuery queryParams) { + void queryParams(UriQuery queryParams) { this.queryParams = queryParams; } void clearTrace() { - setTraceSuccess(true); - setTraceDescription(null); - setTraceThrowable(null); + traceSuccess(true); + traceDescription(null); + traceThrowable(null); } } diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityPreMatchingFilter.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityPreMatchingFilter.java index 45be298e0cc..65a3faef81a 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityPreMatchingFilter.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityPreMatchingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,10 +97,10 @@ protected void processSecurity(ContainerRequestContext request, // when I reach this point, I am sure we should at least authenticate in prematching filter authenticate(filterContext, securityContext, tracing.atnTracing()); - LOGGER.log(Level.TRACE, () -> "Filter after authentication. Should finish: " + filterContext.isShouldFinish()); + LOGGER.log(Level.TRACE, () -> "Filter after authentication. Should finish: " + filterContext.shouldFinish()); // authentication failed - if (filterContext.isShouldFinish()) { + if (filterContext.shouldFinish()) { return; } @@ -122,9 +122,9 @@ protected SecurityFilterContext initRequestFiltering(ContainerRequestContext req SecurityDefinition methodDef = new SecurityDefinition(false, false); methodDef.requiresAuthentication(true); - methodDef.setRequiresAuthorization(featureConfig().shouldUsePrematchingAuthorization()); - context.setMethodSecurity(methodDef); - context.setResourceName("jax-rs"); + methodDef.requiresAuthorization(featureConfig().shouldUsePrematchingAuthorization()); + context.methodSecurity(methodDef); + context.resourceName("jax-rs"); return configureContext(context, requestContext, uriInfo); } diff --git a/microprofile/security/src/main/java/module-info.java b/microprofile/security/src/main/java/module-info.java index ebf03e7c62c..aeceddd027c 100644 --- a/microprofile/security/src/main/java/module-info.java +++ b/microprofile/security/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ @SuppressWarnings({ "requires-automatic", "requires-transitive-automatic" }) module io.helidon.microprofile.security { + requires io.helidon.config.mp; requires io.helidon.jersey.common; requires io.helidon.microprofile.cdi; requires io.helidon.microprofile.server; diff --git a/microprofile/security/src/test/java/io/helidon/microprofile/security/OptionalSecurityTest.java b/microprofile/security/src/test/java/io/helidon/microprofile/security/OptionalSecurityTest.java index 4bb0afe14e1..5e55a002bbc 100644 --- a/microprofile/security/src/test/java/io/helidon/microprofile/security/OptionalSecurityTest.java +++ b/microprofile/security/src/test/java/io/helidon/microprofile/security/OptionalSecurityTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -100,7 +100,7 @@ void testOptional() { */ securityFilter.processAuthentication(filterContext, clientBuilder, methodSecurity, tracing.atnTracing()); - assertThat(filterContext.isShouldFinish(), is(false)); + assertThat(filterContext.shouldFinish(), is(false)); assertThat(securityContext.user(), is(Optional.empty())); } @@ -108,7 +108,7 @@ void testOptional() { void testNotOptional() { SecurityContext securityContext = security.createContext("context_id"); SecurityFilterContext filterContext = new SecurityFilterContext(); - filterContext.setJerseyRequest(mock(ContainerRequest.class)); + filterContext.jerseyRequest(mock(ContainerRequest.class)); SecurityDefinition methodSecurity = mock(SecurityDefinition.class); when(methodSecurity.authenticationOptional()).thenReturn(false); @@ -123,7 +123,7 @@ void testNotOptional() { */ securityFilter.processAuthentication(filterContext, clientBuilder, methodSecurity, tracing.atnTracing()); - assertThat(filterContext.isShouldFinish(), is(true)); + assertThat(filterContext.shouldFinish(), is(true)); assertThat(securityContext.user(), is(Optional.empty())); } diff --git a/microprofile/security/src/test/java/io/helidon/microprofile/security/SecurityFilterTest.java b/microprofile/security/src/test/java/io/helidon/microprofile/security/SecurityFilterTest.java index 4176a55a166..1c66526c140 100644 --- a/microprofile/security/src/test/java/io/helidon/microprofile/security/SecurityFilterTest.java +++ b/microprofile/security/src/test/java/io/helidon/microprofile/security/SecurityFilterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ void testAtnAbortWith() { ContainerRequest request = mock(ContainerRequest.class); SecurityFilterContext filterContext = new SecurityFilterContext(); - filterContext.setJerseyRequest(request); + filterContext.jerseyRequest(request); SecurityDefinition methodSecurity = mock(SecurityDefinition.class); @@ -74,7 +74,7 @@ void testAtnAbortWith() { when(clientBuilder.submit()).thenReturn(AuthenticationResponse.failed("Unit-test")); sf.processAuthentication(filterContext, clientBuilder, methodSecurity, tracing.atnTracing()); - assertThat(filterContext.isShouldFinish(), is(true)); + assertThat(filterContext.shouldFinish(), is(true)); verify(request).abortWith(argThat(response -> response.getStatus() == 401)); } @@ -94,7 +94,7 @@ void testAtnThrowException() { ContainerRequest request = mock(ContainerRequest.class); SecurityFilterContext filterContext = new SecurityFilterContext(); - filterContext.setJerseyRequest(request); + filterContext.jerseyRequest(request); SecurityDefinition methodSecurity = mock(SecurityDefinition.class); @@ -125,7 +125,7 @@ void testAtzAbortWith() { ContainerRequest request = mock(ContainerRequest.class); SecurityFilterContext filterContext = new SecurityFilterContext(); - filterContext.setJerseyRequest(request); + filterContext.jerseyRequest(request); SecurityClientBuilder clientBuilder = mock(SecurityClientBuilder.class); when(clientBuilder.submit()).thenReturn(AuthorizationResponse.builder() @@ -134,7 +134,7 @@ void testAtzAbortWith() { .build()); sf.processAuthorization(filterContext, clientBuilder); - assertThat(filterContext.isShouldFinish(), is(true)); + assertThat(filterContext.shouldFinish(), is(true)); verify(request).abortWith(argThat(response -> response.getStatus() == 403)); } @@ -154,7 +154,7 @@ void testAtzThrowException() { ContainerRequest request = mock(ContainerRequest.class); SecurityFilterContext filterContext = new SecurityFilterContext(); - filterContext.setJerseyRequest(request); + filterContext.jerseyRequest(request); SecurityClientBuilder clientBuilder = mock(SecurityClientBuilder.class); when(clientBuilder.submit()).thenReturn(AuthorizationResponse.builder() diff --git a/microprofile/server/pom.xml b/microprofile/server/pom.xml index 43bc7a6e93c..1238517002f 100644 --- a/microprofile/server/pom.xml +++ b/microprofile/server/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.server helidon-microprofile-server diff --git a/microprofile/service-common/pom.xml b/microprofile/service-common/pom.xml index f34b650cb5d..baffd12cf4f 100644 --- a/microprofile/service-common/pom.xml +++ b/microprofile/service-common/pom.xml @@ -20,7 +20,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/microprofile/telemetry/pom.xml b/microprofile/telemetry/pom.xml index fa3e4bc53ce..cb75c632365 100644 --- a/microprofile/telemetry/pom.xml +++ b/microprofile/telemetry/pom.xml @@ -22,7 +22,7 @@ helidon-microprofile-project io.helidon.microprofile - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.telemetry helidon-microprofile-telemetry diff --git a/microprofile/testing/junit5/pom.xml b/microprofile/testing/junit5/pom.xml index 788f1f11861..40fe01c0dad 100644 --- a/microprofile/testing/junit5/pom.xml +++ b/microprofile/testing/junit5/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.testing helidon-microprofile-testing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-testing-junit5 diff --git a/microprofile/testing/mocking/pom.xml b/microprofile/testing/mocking/pom.xml index baa919f9fde..7b8cbc3b001 100644 --- a/microprofile/testing/mocking/pom.xml +++ b/microprofile/testing/mocking/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.testing helidon-microprofile-testing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-testing-mocking diff --git a/microprofile/testing/pom.xml b/microprofile/testing/pom.xml index 5e2b83b6683..f4fbdb87a39 100644 --- a/microprofile/testing/pom.xml +++ b/microprofile/testing/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.microprofile.testing diff --git a/microprofile/testing/testng/pom.xml b/microprofile/testing/testng/pom.xml index 84a17ced784..9f781af4fc2 100644 --- a/microprofile/testing/testng/pom.xml +++ b/microprofile/testing/testng/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.testing helidon-microprofile-testing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-testing-testng diff --git a/microprofile/tests/arquillian/pom.xml b/microprofile/tests/arquillian/pom.xml index 979d3db768f..44a5ec8a173 100644 --- a/microprofile/tests/arquillian/pom.xml +++ b/microprofile/tests/arquillian/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests helidon-microprofile-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-arquillian Helidon Microprofile Arquillian Integration diff --git a/microprofile/tests/config/pom.xml b/microprofile/tests/config/pom.xml index 78b3b3e1e03..8ffcee52216 100644 --- a/microprofile/tests/config/pom.xml +++ b/microprofile/tests/config/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests helidon-microprofile-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-tests-config Helidon Microprofile Config Tests diff --git a/microprofile/tests/pom.xml b/microprofile/tests/pom.xml index c409b59e0f7..d71ae81b3cc 100644 --- a/microprofile/tests/pom.xml +++ b/microprofile/tests/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.microprofile.tests diff --git a/microprofile/tests/server/pom.xml b/microprofile/tests/server/pom.xml index 82ef7e5dd68..789b8d9f6bf 100644 --- a/microprofile/tests/server/pom.xml +++ b/microprofile/tests/server/pom.xml @@ -23,7 +23,7 @@ io.helidon.microprofile.tests helidon-microprofile-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 helidon-microprofile-tests-server @@ -65,5 +65,10 @@ helidon-logging-jul test + + io.helidon.webclient + helidon-webclient-http1 + test + diff --git a/microprofile/tests/server/src/test/java/io/helidon/microprofile/tests/server/BadHostHeaderTest.java b/microprofile/tests/server/src/test/java/io/helidon/microprofile/tests/server/BadHostHeaderTest.java new file mode 100644 index 00000000000..1efb71706b2 --- /dev/null +++ b/microprofile/tests/server/src/test/java/io/helidon/microprofile/tests/server/BadHostHeaderTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tests.server; + +import io.helidon.http.Header; +import io.helidon.http.HeaderValues; +import io.helidon.http.Status; +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.webclient.api.WebClient; +import io.helidon.webserver.http.ServerRequest; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +@AddBean(BadHostHeaderTest.TestResource.class) +public class BadHostHeaderTest { + private static final Header BAD_HOST_HEADER = HeaderValues.create("Host", "localhost:808a"); + + @Test + void testGetGoodHeader(WebTarget target) { + String getResponse = target.path("/get").request().get(String.class); + assertThat(getResponse, is("localhost")); + } + + @Test + void testGetBadHeader(WebTarget target) { + WebClient webClient = WebClient.builder() + .baseUri(target.getUri()) + .build(); + var response = webClient.get("/get") + .header(BAD_HOST_HEADER) + .request(String.class); + assertThat(response.status(), is(Status.BAD_REQUEST_400)); + assertThat(response.entity(), is("Invalid port of the host header: 808a")); + } + + @Path("/") + public static class TestResource { + @Context ServerRequest request; + + @GET + @Path("get") + @Produces(MediaType.TEXT_PLAIN) + public String getIt() { + return request.requestedUri().host(); + } + } +} diff --git a/microprofile/tests/tck/pom.xml b/microprofile/tests/tck/pom.xml index bd69c003f64..a6b41d491e3 100644 --- a/microprofile/tests/tck/pom.xml +++ b/microprofile/tests/tck/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests helidon-microprofile-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.microprofile.tests.tck diff --git a/microprofile/tests/tck/tck-annotations/pom.xml b/microprofile/tests/tck/tck-annotations/pom.xml index f901bdf77ac..7edfc54c98a 100644 --- a/microprofile/tests/tck/tck-annotations/pom.xml +++ b/microprofile/tests/tck/tck-annotations/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-annotations Helidon Microprofile Tests TCK Annotations diff --git a/microprofile/tests/tck/tck-cdi-lang-model/pom.xml b/microprofile/tests/tck/tck-cdi-lang-model/pom.xml index 9a7b63c25c3..bc4b646bc57 100644 --- a/microprofile/tests/tck/tck-cdi-lang-model/pom.xml +++ b/microprofile/tests/tck/tck-cdi-lang-model/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-cdi-lang-model Helidon Microprofile Tests TCK CDI Lang model diff --git a/microprofile/tests/tck/tck-cdi/pom.xml b/microprofile/tests/tck/tck-cdi/pom.xml index 8a206739591..c22b6cc2a9e 100644 --- a/microprofile/tests/tck/tck-cdi/pom.xml +++ b/microprofile/tests/tck/tck-cdi/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-cdi Helidon Microprofile Tests TCK CDI diff --git a/microprofile/tests/tck/tck-config/pom.xml b/microprofile/tests/tck/tck-config/pom.xml index 179c8486e43..6e0164a84e5 100644 --- a/microprofile/tests/tck/tck-config/pom.xml +++ b/microprofile/tests/tck/tck-config/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-config Helidon Microprofile Tests TCK Config diff --git a/microprofile/tests/tck/tck-core-profile/pom.xml b/microprofile/tests/tck/tck-core-profile/pom.xml index 6611a1c57e7..44092348e5a 100644 --- a/microprofile/tests/tck/tck-core-profile/pom.xml +++ b/microprofile/tests/tck/tck-core-profile/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom helidon-microprofile-tests-tck-core-profile diff --git a/microprofile/tests/tck/tck-core-profile/tck-core-profile-test/pom.xml b/microprofile/tests/tck/tck-core-profile/tck-core-profile-test/pom.xml index 1349f4a1daf..dd4fd624c91 100644 --- a/microprofile/tests/tck/tck-core-profile/tck-core-profile-test/pom.xml +++ b/microprofile/tests/tck/tck-core-profile/tck-core-profile-test/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-core-profile - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-tests-tck-core-profile-test Helidon Microprofile Tests TCK Core Profile diff --git a/microprofile/tests/tck/tck-fault-tolerance/pom.xml b/microprofile/tests/tck/tck-fault-tolerance/pom.xml index 1fd068c1c6b..4d8eb43bbb1 100644 --- a/microprofile/tests/tck/tck-fault-tolerance/pom.xml +++ b/microprofile/tests/tck/tck-fault-tolerance/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-fault-tolerance Helidon Microprofile Tests TCK Fault Tolerance diff --git a/microprofile/tests/tck/tck-graphql/pom.xml b/microprofile/tests/tck/tck-graphql/pom.xml index 63ae713acf3..32e5d9981dc 100644 --- a/microprofile/tests/tck/tck-graphql/pom.xml +++ b/microprofile/tests/tck/tck-graphql/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-graphql Helidon Microprofile Tests TCK GraphQL diff --git a/microprofile/tests/tck/tck-health/pom.xml b/microprofile/tests/tck/tck-health/pom.xml index 39820dc4189..51f3ca254a0 100644 --- a/microprofile/tests/tck/tck-health/pom.xml +++ b/microprofile/tests/tck/tck-health/pom.xml @@ -23,7 +23,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/microprofile/tests/tck/tck-inject/pom.xml b/microprofile/tests/tck/tck-inject/pom.xml index d29aa59b95d..5b333997bf2 100644 --- a/microprofile/tests/tck/tck-inject/pom.xml +++ b/microprofile/tests/tck/tck-inject/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom helidon-microprofile-tests-tck-inject diff --git a/microprofile/tests/tck/tck-inject/tck-inject-test/pom.xml b/microprofile/tests/tck/tck-inject/tck-inject-test/pom.xml index 8a725bf9ac0..0defcc444a5 100644 --- a/microprofile/tests/tck/tck-inject/tck-inject-test/pom.xml +++ b/microprofile/tests/tck/tck-inject/tck-inject-test/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-inject - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-tests-tck-inject-test Helidon Microprofile Tests TCK Inject diff --git a/microprofile/tests/tck/tck-jsonb/pom.xml b/microprofile/tests/tck/tck-jsonb/pom.xml index d21ac59993c..bb1f6865586 100644 --- a/microprofile/tests/tck/tck-jsonb/pom.xml +++ b/microprofile/tests/tck/tck-jsonb/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom helidon-microprofile-tests-tck-jsonb diff --git a/microprofile/tests/tck/tck-jsonb/tck-jsonb-test/pom.xml b/microprofile/tests/tck/tck-jsonb/tck-jsonb-test/pom.xml index 0386f730ecb..f2b76d3ad44 100644 --- a/microprofile/tests/tck/tck-jsonb/tck-jsonb-test/pom.xml +++ b/microprofile/tests/tck/tck-jsonb/tck-jsonb-test/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-jsonb - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-tests-tck-jsonb-test Helidon Microprofile Tests TCK JSONB diff --git a/microprofile/tests/tck/tck-jsonp/pom.xml b/microprofile/tests/tck/tck-jsonp/pom.xml index 177e48f1171..93fa13b9845 100644 --- a/microprofile/tests/tck/tck-jsonp/pom.xml +++ b/microprofile/tests/tck/tck-jsonp/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom helidon-microprofile-tests-tck-jsonp diff --git a/microprofile/tests/tck/tck-jsonp/tck-jsonp-pluggability-test/pom.xml b/microprofile/tests/tck/tck-jsonp/tck-jsonp-pluggability-test/pom.xml index 9bb77957b48..97410e1b438 100644 --- a/microprofile/tests/tck/tck-jsonp/tck-jsonp-pluggability-test/pom.xml +++ b/microprofile/tests/tck/tck-jsonp/tck-jsonp-pluggability-test/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-jsonp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-tests-tck-jsonp-pluggability-test Helidon Microprofile Tests TCK JSONP diff --git a/microprofile/tests/tck/tck-jsonp/tck-jsonp-test/pom.xml b/microprofile/tests/tck/tck-jsonp/tck-jsonp-test/pom.xml index ff477d86d40..8fce844b494 100644 --- a/microprofile/tests/tck/tck-jsonp/tck-jsonp-test/pom.xml +++ b/microprofile/tests/tck/tck-jsonp/tck-jsonp-test/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-jsonp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-tests-tck-jsonp-test Helidon Microprofile Tests TCK JSONP diff --git a/microprofile/tests/tck/tck-jwt-auth/pom.xml b/microprofile/tests/tck/tck-jwt-auth/pom.xml index 2cc820921be..7fff4a8c0a2 100644 --- a/microprofile/tests/tck/tck-jwt-auth/pom.xml +++ b/microprofile/tests/tck/tck-jwt-auth/pom.xml @@ -21,7 +21,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-jwt-auth Helidon Microprofile Tests TCK JWT-Auth diff --git a/microprofile/tests/tck/tck-lra/pom.xml b/microprofile/tests/tck/tck-lra/pom.xml index 98dbf742b40..a21d64af366 100644 --- a/microprofile/tests/tck/tck-lra/pom.xml +++ b/microprofile/tests/tck/tck-lra/pom.xml @@ -21,7 +21,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-lra diff --git a/microprofile/tests/tck/tck-messaging/pom.xml b/microprofile/tests/tck/tck-messaging/pom.xml index 409ba4cc64d..820cdc74673 100644 --- a/microprofile/tests/tck/tck-messaging/pom.xml +++ b/microprofile/tests/tck/tck-messaging/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-messaging Helidon Microprofile Tests TCK Messaging diff --git a/microprofile/tests/tck/tck-metrics/pom.xml b/microprofile/tests/tck/tck-metrics/pom.xml index e90782b5743..869504c18b2 100644 --- a/microprofile/tests/tck/tck-metrics/pom.xml +++ b/microprofile/tests/tck/tck-metrics/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-metrics Helidon Microprofile Tests TCK Metrics diff --git a/microprofile/tests/tck/tck-openapi/pom.xml b/microprofile/tests/tck/tck-openapi/pom.xml index 97496350777..ce5a4d6e1b4 100644 --- a/microprofile/tests/tck/tck-openapi/pom.xml +++ b/microprofile/tests/tck/tck-openapi/pom.xml @@ -23,7 +23,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/microprofile/tests/tck/tck-opentracing/pom.xml b/microprofile/tests/tck/tck-opentracing/pom.xml index 2e12cd0415a..649a05bf4b0 100644 --- a/microprofile/tests/tck/tck-opentracing/pom.xml +++ b/microprofile/tests/tck/tck-opentracing/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-opentracing Helidon Microprofile Tests TCK Opentracing diff --git a/microprofile/tests/tck/tck-reactive-operators/pom.xml b/microprofile/tests/tck/tck-reactive-operators/pom.xml index 79a003ca564..e843cf99a5c 100644 --- a/microprofile/tests/tck/tck-reactive-operators/pom.xml +++ b/microprofile/tests/tck/tck-reactive-operators/pom.xml @@ -23,7 +23,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT tck-reactive-operators Helidon Microprofile Tests TCK Reactive Operators diff --git a/microprofile/tests/tck/tck-rest-client/pom.xml b/microprofile/tests/tck/tck-rest-client/pom.xml index 75b5a0a9d75..a95eace7734 100644 --- a/microprofile/tests/tck/tck-rest-client/pom.xml +++ b/microprofile/tests/tck/tck-rest-client/pom.xml @@ -21,7 +21,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/microprofile/tests/tck/tck-restful/pom.xml b/microprofile/tests/tck/tck-restful/pom.xml index f3c3e2a6ea1..f45fa352ebe 100644 --- a/microprofile/tests/tck/tck-restful/pom.xml +++ b/microprofile/tests/tck/tck-restful/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom tck-restful diff --git a/microprofile/tests/tck/tck-restful/tck-restful-test/pom.xml b/microprofile/tests/tck/tck-restful/tck-restful-test/pom.xml index c9ac94f793c..2128082717a 100644 --- a/microprofile/tests/tck/tck-restful/tck-restful-test/pom.xml +++ b/microprofile/tests/tck/tck-restful/tck-restful-test/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.tests.tck tck-restful - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-tests-tck-restful-test Helidon Microprofile Tests TCK Restful diff --git a/microprofile/tests/tck/tck-telemetry/pom.xml b/microprofile/tests/tck/tck-telemetry/pom.xml index a41b2577b2a..a85d8c7e99e 100644 --- a/microprofile/tests/tck/tck-telemetry/pom.xml +++ b/microprofile/tests/tck/tck-telemetry/pom.xml @@ -21,7 +21,7 @@ io.helidon.microprofile.tests.tck helidon-microprofile-tests-tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/microprofile/tests/telemetry/pom.xml b/microprofile/tests/telemetry/pom.xml index 03adc04cd7d..80d90e09843 100644 --- a/microprofile/tests/telemetry/pom.xml +++ b/microprofile/tests/telemetry/pom.xml @@ -21,7 +21,7 @@ io.helidon.microprofile.tests helidon-microprofile-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 helidon-microprofile-tests-telemetry diff --git a/microprofile/tests/testing/junit5/pom.xml b/microprofile/tests/testing/junit5/pom.xml index 6e1a3d1b6fd..3c48ca8a3e8 100644 --- a/microprofile/tests/testing/junit5/pom.xml +++ b/microprofile/tests/testing/junit5/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile.tests.testing helidon-microprofile-tests-testing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-tests-testing-junit5 diff --git a/microprofile/tests/testing/pom.xml b/microprofile/tests/testing/pom.xml index 5077722b737..c177242051e 100644 --- a/microprofile/tests/testing/pom.xml +++ b/microprofile/tests/testing/pom.xml @@ -23,9 +23,9 @@ io.helidon.microprofile.tests helidon-microprofile-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 pom io.helidon.microprofile.tests.testing diff --git a/microprofile/tests/testing/testng/pom.xml b/microprofile/tests/testing/testng/pom.xml index 3a49dbefb8d..8b439c66b58 100644 --- a/microprofile/tests/testing/testng/pom.xml +++ b/microprofile/tests/testing/testng/pom.xml @@ -21,7 +21,7 @@ io.helidon.microprofile.tests.testing helidon-microprofile-tests-testing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-microprofile-tests-testing-testng diff --git a/microprofile/tracing/pom.xml b/microprofile/tracing/pom.xml index 4abedf4e277..947afe315a2 100644 --- a/microprofile/tracing/pom.xml +++ b/microprofile/tracing/pom.xml @@ -22,7 +22,7 @@ helidon-microprofile-project io.helidon.microprofile - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.tracing helidon-microprofile-tracing diff --git a/microprofile/websocket/pom.xml b/microprofile/websocket/pom.xml index a0972b0188f..ab00465935a 100644 --- a/microprofile/websocket/pom.xml +++ b/microprofile/websocket/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.microprofile.websocket @@ -76,6 +76,10 @@ jakarta.websocket jakarta.websocket-client-api + + io.helidon.common.concurrency + helidon-common-concurrency-limits + io.helidon.common.features helidon-common-features-api diff --git a/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusConnection.java b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusConnection.java index 581f8d0abeb..3172a3cd32c 100644 --- a/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusConnection.java +++ b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusConnection.java @@ -25,8 +25,12 @@ import io.helidon.common.buffers.BufferData; import io.helidon.common.buffers.DataReader; +import io.helidon.common.concurrency.limits.FixedLimit; +import io.helidon.common.concurrency.limits.Limit; +import io.helidon.common.concurrency.limits.LimitException; import io.helidon.common.socket.SocketContext; import io.helidon.http.DateTime; +import io.helidon.webserver.CloseConnectionException; import io.helidon.webserver.ConnectionContext; import io.helidon.webserver.spi.ServerConnection; import io.helidon.websocket.WsCloseCodes; @@ -67,33 +71,45 @@ class TyrusConnection implements ServerConnection, WsSession { } @Override - public void handle(Semaphore requestSemaphore) { + public void handle(Limit limit) { myThread = Thread.currentThread(); DataReader dataReader = ctx.dataReader(); - listener.onOpen(this); - if (requestSemaphore.tryAcquire()) { + + try { + limit.invoke(() -> listener.onOpen(this)); + } catch (LimitException e) { + listener.onError(this, e); + throw new CloseConnectionException("Too many concurrent requests"); + } catch (Exception e) { + listener.onError(this, e); + listener.onClose(this, WsCloseCodes.UNEXPECTED_CONDITION, e.getMessage()); + return; + } + + while (canRun) { try { - while (canRun) { - try { - readingNetwork = true; - BufferData buffer = dataReader.readBuffer(); - readingNetwork = false; - lastRequestTimestamp = DateTime.timestamp(); - listener.onMessage(this, buffer, true); - lastRequestTimestamp = DateTime.timestamp(); - } catch (Exception e) { - listener.onError(this, e); - listener.onClose(this, WsCloseCodes.UNEXPECTED_CONDITION, e.getMessage()); - return; - } - } - listener.onClose(this, WsCloseCodes.NORMAL_CLOSE, "Idle timeout"); - } finally { - requestSemaphore.release(); + readingNetwork = true; + BufferData buffer = dataReader.readBuffer(); + readingNetwork = false; + lastRequestTimestamp = DateTime.timestamp(); + limit.invoke(() -> listener.onMessage(this, buffer, true)); + lastRequestTimestamp = DateTime.timestamp(); + } catch (LimitException e) { + listener.onClose(this, WsCloseCodes.TRY_AGAIN_LATER, "Too Many Concurrent Requests"); + return; + } catch (Exception e) { + listener.onError(this, e); + listener.onClose(this, WsCloseCodes.UNEXPECTED_CONDITION, e.getMessage()); + return; } - } else { - listener.onClose(this, WsCloseCodes.TRY_AGAIN_LATER, "Too Many Concurrent Requests"); } + listener.onClose(this, WsCloseCodes.NORMAL_CLOSE, "Idle timeout"); + } + + @SuppressWarnings("removal") + @Override + public void handle(Semaphore requestSemaphore) { + handle(FixedLimit.create(requestSemaphore)); } @Override diff --git a/microprofile/websocket/src/main/java/module-info.java b/microprofile/websocket/src/main/java/module-info.java index 939671a2800..da725db8de5 100644 --- a/microprofile/websocket/src/main/java/module-info.java +++ b/microprofile/websocket/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ requires static io.helidon.common.features.api; requires transitive jakarta.websocket; + requires transitive io.helidon.common.concurrency.limits; exports io.helidon.microprofile.tyrus; diff --git a/microprofile/weld/pom.xml b/microprofile/weld/pom.xml index 46835c5d20d..7c69182580f 100644 --- a/microprofile/weld/pom.xml +++ b/microprofile/weld/pom.xml @@ -22,7 +22,7 @@ io.helidon.microprofile helidon-microprofile-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/microprofile/weld/weld-core-impl/pom.xml b/microprofile/weld/weld-core-impl/pom.xml index 1184c011477..a4bdbf1e0bf 100644 --- a/microprofile/weld/weld-core-impl/pom.xml +++ b/microprofile/weld/weld-core-impl/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.weld helidon-microprofile-weld-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT weld-core-impl Helidon Weld SE Impl diff --git a/microprofile/weld/weld-se-core/pom.xml b/microprofile/weld/weld-se-core/pom.xml index 0a20e272ae6..fd50544c174 100644 --- a/microprofile/weld/weld-se-core/pom.xml +++ b/microprofile/weld/weld-se-core/pom.xml @@ -24,7 +24,7 @@ io.helidon.microprofile.weld helidon-microprofile-weld-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT weld-se-core Helidon Weld SE Core diff --git a/openapi/openapi/pom.xml b/openapi/openapi/pom.xml index 1b016979bea..673ac2be23c 100644 --- a/openapi/openapi/pom.xml +++ b/openapi/openapi/pom.xml @@ -21,7 +21,7 @@ io.helidon.openapi helidon-openapi-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-openapi diff --git a/openapi/pom.xml b/openapi/pom.xml index 09b56ad512e..0c71bb86c19 100644 --- a/openapi/pom.xml +++ b/openapi/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.openapi diff --git a/openapi/tests/gh-5792/pom.xml b/openapi/tests/gh-5792/pom.xml index 10215aa64ee..380b7e9a2b3 100644 --- a/openapi/tests/gh-5792/pom.xml +++ b/openapi/tests/gh-5792/pom.xml @@ -24,7 +24,7 @@ io.helidon.openapi.tests helidon-openapi-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-openapi-tests-yaml-parsing diff --git a/openapi/tests/pom.xml b/openapi/tests/pom.xml index 869e0131cce..369f21afe3d 100644 --- a/openapi/tests/pom.xml +++ b/openapi/tests/pom.xml @@ -24,7 +24,7 @@ io.helidon.openapi helidon-openapi-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.openapi.tests diff --git a/parent/pom.xml b/parent/pom.xml index d41cbb96500..2430c5d6c16 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -22,7 +22,7 @@ 4.0.0 io.helidon helidon-parent - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom Helidon Parent @@ -168,7 +168,7 @@ io.helidon.build-tools helidon-build-cache-maven-plugin - 4.0.11 + 4.0.14 @@ -199,7 +199,7 @@ - staging + ossrh-staging ossrh-staging diff --git a/pom.xml b/pom.xml index 2e175a2ba98..0e0503a8b07 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ io.helidon helidon-dependencies - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ./dependencies/pom.xml helidon-project @@ -65,7 +65,7 @@ 1.7.0.Final 9.7 10.12.5 - 2.11.0 + 2.14.0 2.4.14 10.0.3 1.4 @@ -117,7 +117,7 @@ 3.1.0 3.1.2 2.3 - 4.0.11 + 4.0.14 ${version.lib.hibernate} 3.1.2 0.8.5 @@ -135,7 +135,7 @@ 3.0.1 4.7.3.5 1.12.0 - 10.0.3 + 10.0.4 3.1.0 1.1 2.3 diff --git a/scheduling/pom.xml b/scheduling/pom.xml index 1461040a9df..e509dd38d81 100644 --- a/scheduling/pom.xml +++ b/scheduling/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.scheduling diff --git a/security/abac/policy-el/pom.xml b/security/abac/policy-el/pom.xml index ef4447f0583..4a8518c6f21 100644 --- a/security/abac/policy-el/pom.xml +++ b/security/abac/policy-el/pom.xml @@ -23,7 +23,7 @@ io.helidon.security.abac helidon-security-abac-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 helidon-security-abac-policy-el diff --git a/security/abac/policy/pom.xml b/security/abac/policy/pom.xml index 1bddf59134a..5c87114cea4 100644 --- a/security/abac/policy/pom.xml +++ b/security/abac/policy/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.abac helidon-security-abac-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-abac-policy Helidon Security Validators Policy diff --git a/security/abac/pom.xml b/security/abac/pom.xml index 28c7b109615..75e4b7aa68f 100644 --- a/security/abac/pom.xml +++ b/security/abac/pom.xml @@ -23,7 +23,7 @@ io.helidon.security helidon-security-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/security/abac/role/pom.xml b/security/abac/role/pom.xml index d4b02529247..180e646ab51 100644 --- a/security/abac/role/pom.xml +++ b/security/abac/role/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.abac helidon-security-abac-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-abac-role Helidon Security Validators Role diff --git a/security/abac/scope/pom.xml b/security/abac/scope/pom.xml index 03283e78cfe..168b28ba86e 100644 --- a/security/abac/scope/pom.xml +++ b/security/abac/scope/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.abac helidon-security-abac-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-abac-scope Helidon Security Validators Scope diff --git a/security/abac/time/pom.xml b/security/abac/time/pom.xml index 8d3a3a963f2..98d5eac7f0f 100644 --- a/security/abac/time/pom.xml +++ b/security/abac/time/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.abac helidon-security-abac-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-abac-time Helidon Security Validators Time diff --git a/security/annotations/pom.xml b/security/annotations/pom.xml index 9fd32380ae1..3ce07e4ce89 100644 --- a/security/annotations/pom.xml +++ b/security/annotations/pom.xml @@ -24,7 +24,7 @@ io.helidon.security helidon-security-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-annotations Helidon Security Integration Annotations diff --git a/security/integration/common/pom.xml b/security/integration/common/pom.xml index dec522d9bf1..d2310a916ce 100644 --- a/security/integration/common/pom.xml +++ b/security/integration/common/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.integration helidon-security-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-integration-common Helidon Security Integration Common diff --git a/security/integration/pom.xml b/security/integration/pom.xml index a5fac28050d..c61cb420ad9 100644 --- a/security/integration/pom.xml +++ b/security/integration/pom.xml @@ -24,7 +24,7 @@ io.helidon.security helidon-security-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/security/jwt/pom.xml b/security/jwt/pom.xml index 5d7eb057e29..6fcd22d8fbe 100644 --- a/security/jwt/pom.xml +++ b/security/jwt/pom.xml @@ -24,7 +24,7 @@ io.helidon.security helidon-security-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-jwt Helidon Security JWT diff --git a/security/pom.xml b/security/pom.xml index 43e3970c756..a1f44e917ef 100644 --- a/security/pom.xml +++ b/security/pom.xml @@ -24,7 +24,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.security diff --git a/security/providers/abac/pom.xml b/security/providers/abac/pom.xml index eb906e2e798..94e2fd64b8c 100644 --- a/security/providers/abac/pom.xml +++ b/security/providers/abac/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.providers helidon-security-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-providers-abac Helidon Security Providers ABAC diff --git a/security/providers/common/pom.xml b/security/providers/common/pom.xml index 9a70ea9df1b..0423a51b68d 100644 --- a/security/providers/common/pom.xml +++ b/security/providers/common/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.providers helidon-security-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-providers-common Helidon Security Providers Common diff --git a/security/providers/config-vault/pom.xml b/security/providers/config-vault/pom.xml index 113153474e1..da667dcd5be 100644 --- a/security/providers/config-vault/pom.xml +++ b/security/providers/config-vault/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.providers helidon-security-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-providers-config-vault Helidon Security Providers Config Vault diff --git a/security/providers/google-login/pom.xml b/security/providers/google-login/pom.xml index ee0ba444c41..e4894609a80 100644 --- a/security/providers/google-login/pom.xml +++ b/security/providers/google-login/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.providers helidon-security-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-providers-google-login Helidon Security Providers Google Login diff --git a/security/providers/header/pom.xml b/security/providers/header/pom.xml index ff9fde5dd51..f7c20e8df99 100644 --- a/security/providers/header/pom.xml +++ b/security/providers/header/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.providers helidon-security-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-providers-header Helidon Security Providers Header authentication diff --git a/security/providers/http-auth/pom.xml b/security/providers/http-auth/pom.xml index 9b5a7643187..baa9f241fda 100644 --- a/security/providers/http-auth/pom.xml +++ b/security/providers/http-auth/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.providers helidon-security-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-providers-http-auth Helidon Security Providers HTTP Authentication diff --git a/security/providers/http-sign/pom.xml b/security/providers/http-sign/pom.xml index c03bf298b61..d46f65ff572 100644 --- a/security/providers/http-sign/pom.xml +++ b/security/providers/http-sign/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.providers helidon-security-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-providers-http-sign Helidon Security Providers HTTP Signature diff --git a/security/providers/idcs-mapper/pom.xml b/security/providers/idcs-mapper/pom.xml index 3e2041ccff6..dafdb692a8c 100644 --- a/security/providers/idcs-mapper/pom.xml +++ b/security/providers/idcs-mapper/pom.xml @@ -21,7 +21,7 @@ io.helidon.security.providers helidon-security-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/security/providers/jwt/pom.xml b/security/providers/jwt/pom.xml index e711f38b58e..216677268b9 100644 --- a/security/providers/jwt/pom.xml +++ b/security/providers/jwt/pom.xml @@ -24,7 +24,7 @@ io.helidon.security.providers helidon-security-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-providers-jwt Helidon Security Providers JWT diff --git a/security/providers/oidc-common/pom.xml b/security/providers/oidc-common/pom.xml index 9bea3aa1634..568214d659a 100644 --- a/security/providers/oidc-common/pom.xml +++ b/security/providers/oidc-common/pom.xml @@ -21,7 +21,7 @@ io.helidon.security.providers helidon-security-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 helidon-security-providers-oidc-common diff --git a/security/providers/oidc/pom.xml b/security/providers/oidc/pom.xml index 2b3095a28c1..0750ec066e9 100644 --- a/security/providers/oidc/pom.xml +++ b/security/providers/oidc/pom.xml @@ -23,7 +23,7 @@ io.helidon.security.providers helidon-security-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 helidon-security-providers-oidc diff --git a/security/providers/pom.xml b/security/providers/pom.xml index 3aa2479302e..210caff1fc6 100644 --- a/security/providers/pom.xml +++ b/security/providers/pom.xml @@ -24,7 +24,7 @@ io.helidon.security helidon-security-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.security.providers helidon-security-providers-project diff --git a/security/security/pom.xml b/security/security/pom.xml index 1a2aeeb02e8..c62c640c2fa 100644 --- a/security/security/pom.xml +++ b/security/security/pom.xml @@ -24,7 +24,7 @@ io.helidon.security helidon-security-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security Helidon Security diff --git a/security/util/pom.xml b/security/util/pom.xml index ab205d55c09..bf808c1a581 100644 --- a/security/util/pom.xml +++ b/security/util/pom.xml @@ -24,7 +24,7 @@ io.helidon.security helidon-security-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-security-util Helidon Security Utilities diff --git a/service/codegen/pom.xml b/service/codegen/pom.xml index 2ba57ed268f..4ce71e87d91 100644 --- a/service/codegen/pom.xml +++ b/service/codegen/pom.xml @@ -23,7 +23,7 @@ io.helidon.service helidon-service-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/service/metadata/pom.xml b/service/metadata/pom.xml index 80c50c75729..785dd4b3dac 100644 --- a/service/metadata/pom.xml +++ b/service/metadata/pom.xml @@ -23,7 +23,7 @@ io.helidon.service helidon-service-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/service/pom.xml b/service/pom.xml index 118ca67fdd6..db67a8faa3f 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -24,7 +24,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.service diff --git a/service/registry/pom.xml b/service/registry/pom.xml index 76b62d33456..500631ccdd8 100644 --- a/service/registry/pom.xml +++ b/service/registry/pom.xml @@ -23,7 +23,7 @@ io.helidon.service helidon-service-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/service/tests/codegen/pom.xml b/service/tests/codegen/pom.xml index e7a7eae89f3..13a4c39c24b 100644 --- a/service/tests/codegen/pom.xml +++ b/service/tests/codegen/pom.xml @@ -23,7 +23,7 @@ io.helidon.service.tests helidon-service-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/service/tests/pom.xml b/service/tests/pom.xml index 96051764aab..245ccea4b69 100644 --- a/service/tests/pom.xml +++ b/service/tests/pom.xml @@ -22,7 +22,7 @@ io.helidon.service helidon-service-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/service/tests/registry/pom.xml b/service/tests/registry/pom.xml index 0a66188f428..219a7a56447 100644 --- a/service/tests/registry/pom.xml +++ b/service/tests/registry/pom.xml @@ -22,7 +22,7 @@ io.helidon.service.tests helidon-service-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/apps/bookstore/bookstore-mp/pom.xml b/tests/apps/bookstore/bookstore-mp/pom.xml index f3e868178e0..49d11a6aa8e 100644 --- a/tests/apps/bookstore/bookstore-mp/pom.xml +++ b/tests/apps/bookstore/bookstore-mp/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-mp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../../applications/mp/pom.xml io.helidon.tests.apps.bookstore.bookstore-mp diff --git a/tests/apps/bookstore/bookstore-se/pom.xml b/tests/apps/bookstore/bookstore-se/pom.xml index 24ae2da6f4c..5fb8e21f406 100644 --- a/tests/apps/bookstore/bookstore-se/pom.xml +++ b/tests/apps/bookstore/bookstore-se/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-se - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../../applications/se/pom.xml io.helidon.tests.apps.bookstore.bookstore-se diff --git a/tests/apps/bookstore/common/pom.xml b/tests/apps/bookstore/common/pom.xml index b25cd0f314c..8e117b33e77 100644 --- a/tests/apps/bookstore/common/pom.xml +++ b/tests/apps/bookstore/common/pom.xml @@ -23,7 +23,7 @@ io.helidon.applications helidon-se - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../../applications/se/pom.xml io.helidon.tests.apps.bookstore.common diff --git a/tests/apps/bookstore/pom.xml b/tests/apps/bookstore/pom.xml index 994e284190a..ad39a1e6de2 100644 --- a/tests/apps/bookstore/pom.xml +++ b/tests/apps/bookstore/pom.xml @@ -24,7 +24,7 @@ io.helidon.tests.apps helidon-tests-apps-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.apps.bookstore diff --git a/tests/apps/pom.xml b/tests/apps/pom.xml index 1a3091e1a40..5553b9b0cf9 100644 --- a/tests/apps/pom.xml +++ b/tests/apps/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests helidon-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.apps helidon-tests-apps-project diff --git a/tests/benchmark/jmh/pom.xml b/tests/benchmark/jmh/pom.xml index ad05c95f711..46184d66eae 100644 --- a/tests/benchmark/jmh/pom.xml +++ b/tests/benchmark/jmh/pom.xml @@ -21,7 +21,7 @@ io.helidon.tests.benchmark helidon-tests-benchmark-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-benchmark-jmh diff --git a/tests/benchmark/pom.xml b/tests/benchmark/pom.xml index 0120617e48d..f8bdc436a9a 100644 --- a/tests/benchmark/pom.xml +++ b/tests/benchmark/pom.xml @@ -21,7 +21,7 @@ io.helidon.tests helidon-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.benchmark diff --git a/tests/functional/bookstore/pom.xml b/tests/functional/bookstore/pom.xml index 84c8e77108a..b19bf119cbd 100644 --- a/tests/functional/bookstore/pom.xml +++ b/tests/functional/bookstore/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.functional helidon-tests-functional-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.functional.bookstore helidon-tests-functional-bookstore diff --git a/tests/functional/bookstore/src/test/java/io/helidon/tests/bookstore/MainTest.java b/tests/functional/bookstore/src/test/java/io/helidon/tests/bookstore/MainTest.java index 3592a504a35..eed2ed38863 100644 --- a/tests/functional/bookstore/src/test/java/io/helidon/tests/bookstore/MainTest.java +++ b/tests/functional/bookstore/src/test/java/io/helidon/tests/bookstore/MainTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.lang.System.Logger.Level; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.nio.file.Paths; import java.util.ArrayList; @@ -87,11 +88,11 @@ void stop() throws Exception { } URL getHealthUrl() throws MalformedURLException { - return new URL("http://localhost:" + this.port + "/health"); + return URI.create("http://localhost:" + this.port + "/health").toURL(); } URL getBaseUrl() throws MalformedURLException { - return new URL("http://localhost:" + this.port); + return URI.create("http://localhost:" + this.port).toURL(); } void waitForApplicationDown() throws Exception { @@ -210,7 +211,7 @@ private void runExitOnStartedTest(String edition) throws Exception { Thread.sleep(500); } while (System.currentTimeMillis() < maxTime); - String eol = System.getProperty("line.separator"); + String eol = System.lineSeparator(); Assertions.fail("quickstart " + edition + " did not exit as expected." + eol + eol + "stdOut: " + stdOut + eol diff --git a/tests/functional/config-profiles/pom.xml b/tests/functional/config-profiles/pom.xml index 5f93dadd05b..9b10b4dd141 100644 --- a/tests/functional/config-profiles/pom.xml +++ b/tests/functional/config-profiles/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.functional helidon-tests-functional-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.functional.configprofile helidon-tests-functional-configprofile diff --git a/tests/functional/context-propagation/pom.xml b/tests/functional/context-propagation/pom.xml index 1f326134ed4..a3475b9d11a 100644 --- a/tests/functional/context-propagation/pom.xml +++ b/tests/functional/context-propagation/pom.xml @@ -22,7 +22,7 @@ helidon-tests-functional-project io.helidon.tests.functional - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-functional-context-propagation diff --git a/tests/functional/jax-rs-multiple-apps/pom.xml b/tests/functional/jax-rs-multiple-apps/pom.xml index 37acd4003e3..0a2572516f4 100644 --- a/tests/functional/jax-rs-multiple-apps/pom.xml +++ b/tests/functional/jax-rs-multiple-apps/pom.xml @@ -22,7 +22,7 @@ helidon-tests-functional-project io.helidon.tests.functional - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-functional-jax-rs-multiple-apps diff --git a/tests/functional/jax-rs-subresource/pom.xml b/tests/functional/jax-rs-subresource/pom.xml index 26fa0c56d9e..8f49f98a259 100644 --- a/tests/functional/jax-rs-subresource/pom.xml +++ b/tests/functional/jax-rs-subresource/pom.xml @@ -22,7 +22,7 @@ helidon-tests-functional-project io.helidon.tests.functional - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-functional-jax-rs-subresource Helidon Tests Functional JAX-RS Subresources diff --git a/tests/functional/mp-compression/pom.xml b/tests/functional/mp-compression/pom.xml index 14553630d98..59c70828aee 100644 --- a/tests/functional/mp-compression/pom.xml +++ b/tests/functional/mp-compression/pom.xml @@ -22,7 +22,7 @@ helidon-tests-functional-project io.helidon.tests.functional - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-functional-mp-compression Helidon Tests Functional HTTP compression diff --git a/tests/functional/mp-synthetic-app/pom.xml b/tests/functional/mp-synthetic-app/pom.xml index a9a3eb26218..f0035b0c575 100644 --- a/tests/functional/mp-synthetic-app/pom.xml +++ b/tests/functional/mp-synthetic-app/pom.xml @@ -22,7 +22,7 @@ helidon-tests-functional-project io.helidon.tests.functional - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-functional-mp-synthetic-app Helidon Tests Functional MP Synthetic Application diff --git a/tests/functional/multiport/pom.xml b/tests/functional/multiport/pom.xml index eda9c7315ae..59b357a4fb6 100644 --- a/tests/functional/multiport/pom.xml +++ b/tests/functional/multiport/pom.xml @@ -22,7 +22,7 @@ helidon-tests-functional-project io.helidon.tests.functional - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-functional-multiport Helidon Tests Functional Multiport with MP diff --git a/tests/functional/param-converter-provider/pom.xml b/tests/functional/param-converter-provider/pom.xml index 21244a55c17..6f388f74204 100644 --- a/tests/functional/param-converter-provider/pom.xml +++ b/tests/functional/param-converter-provider/pom.xml @@ -22,7 +22,7 @@ helidon-tests-functional-project io.helidon.tests.functional - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-param-converter-provider diff --git a/tests/functional/pom.xml b/tests/functional/pom.xml index 5ff5cf64bb0..5d6dbadd30a 100644 --- a/tests/functional/pom.xml +++ b/tests/functional/pom.xml @@ -24,7 +24,7 @@ io.helidon.tests helidon-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.functional diff --git a/tests/functional/request-scope-cdi/pom.xml b/tests/functional/request-scope-cdi/pom.xml index ab3c328792d..c31b7381e55 100644 --- a/tests/functional/request-scope-cdi/pom.xml +++ b/tests/functional/request-scope-cdi/pom.xml @@ -22,7 +22,7 @@ helidon-tests-functional-project io.helidon.tests.functional - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-functional-request-scope-cdi diff --git a/tests/functional/request-scope-injection/pom.xml b/tests/functional/request-scope-injection/pom.xml index a05da71fbdf..e20cb65bbfb 100644 --- a/tests/functional/request-scope-injection/pom.xml +++ b/tests/functional/request-scope-injection/pom.xml @@ -22,7 +22,7 @@ helidon-tests-functional-project io.helidon.tests.functional - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-functional-request-scope-injection diff --git a/tests/functional/request-scope/pom.xml b/tests/functional/request-scope/pom.xml index 603d5d6ed88..f94d4e41545 100644 --- a/tests/functional/request-scope/pom.xml +++ b/tests/functional/request-scope/pom.xml @@ -22,7 +22,7 @@ helidon-tests-functional-project io.helidon.tests.functional - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-functional-request-scope diff --git a/tests/integration/config/gh-2171-yml/pom.xml b/tests/integration/config/gh-2171-yml/pom.xml index cc8cf601c64..4be7f49f2b0 100644 --- a/tests/integration/config/gh-2171-yml/pom.xml +++ b/tests/integration/config/gh-2171-yml/pom.xml @@ -20,7 +20,7 @@ io.helidon.tests.integration helidon-tests-integration-config - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/config/gh-2171/pom.xml b/tests/integration/config/gh-2171/pom.xml index 2c0dc8129e0..03afe24ac31 100644 --- a/tests/integration/config/gh-2171/pom.xml +++ b/tests/integration/config/gh-2171/pom.xml @@ -20,7 +20,7 @@ io.helidon.tests.integration helidon-tests-integration-config - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/config/gh-4375/pom.xml b/tests/integration/config/gh-4375/pom.xml index 87556183304..8dcbf67aeb2 100644 --- a/tests/integration/config/gh-4375/pom.xml +++ b/tests/integration/config/gh-4375/pom.xml @@ -24,7 +24,7 @@ io.helidon.tests.integration helidon-tests-integration-config - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-config-gh-4375 diff --git a/tests/integration/config/hocon-mp/pom.xml b/tests/integration/config/hocon-mp/pom.xml index 93efd367759..ac103b534de 100644 --- a/tests/integration/config/hocon-mp/pom.xml +++ b/tests/integration/config/hocon-mp/pom.xml @@ -24,7 +24,7 @@ io.helidon.tests.integration helidon-tests-integration-config - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-config-hocon-mp diff --git a/tests/integration/config/pom.xml b/tests/integration/config/pom.xml index a5c3dd3fbb6..9513f6beea2 100644 --- a/tests/integration/config/pom.xml +++ b/tests/integration/config/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/tests/integration/dbclient/common/pom.xml b/tests/integration/dbclient/common/pom.xml index ae04c67ab2d..5eac060ef25 100644 --- a/tests/integration/dbclient/common/pom.xml +++ b/tests/integration/dbclient/common/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.dbclient helidon-tests-integration-dbclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-dbclient-common Helidon Tests Integration DbClient Common diff --git a/tests/integration/dbclient/h2/pom.xml b/tests/integration/dbclient/h2/pom.xml index c5880b5c983..afbc7eb0791 100644 --- a/tests/integration/dbclient/h2/pom.xml +++ b/tests/integration/dbclient/h2/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration.dbclient helidon-tests-integration-dbclient-parent - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../parent/pom.xml helidon-tests-integration-dbclient-h2 diff --git a/tests/integration/dbclient/mongodb/pom.xml b/tests/integration/dbclient/mongodb/pom.xml index 676b1d57dc2..fc50bbe57cb 100644 --- a/tests/integration/dbclient/mongodb/pom.xml +++ b/tests/integration/dbclient/mongodb/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration.dbclient helidon-tests-integration-dbclient-parent - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../parent/pom.xml helidon-tests-integration-dbclient-mongodb diff --git a/tests/integration/dbclient/mysql/pom.xml b/tests/integration/dbclient/mysql/pom.xml index de3440248ae..932e40ed820 100644 --- a/tests/integration/dbclient/mysql/pom.xml +++ b/tests/integration/dbclient/mysql/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration.dbclient helidon-tests-integration-dbclient-parent - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../parent/pom.xml helidon-tests-integration-dbclient-mysql diff --git a/tests/integration/dbclient/oracle/pom.xml b/tests/integration/dbclient/oracle/pom.xml index 114e15d4097..776a3679afc 100644 --- a/tests/integration/dbclient/oracle/pom.xml +++ b/tests/integration/dbclient/oracle/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration.dbclient helidon-tests-integration-dbclient-parent - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../parent/pom.xml helidon-tests-integration-dbclient-oracle diff --git a/tests/integration/dbclient/parent/pom.xml b/tests/integration/dbclient/parent/pom.xml index 2acb24b05ed..28fc904cba0 100644 --- a/tests/integration/dbclient/parent/pom.xml +++ b/tests/integration/dbclient/parent/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-se - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../../applications/se/pom.xml io.helidon.tests.integration.dbclient diff --git a/tests/integration/dbclient/pgsql/etc/docker/Dockerfile b/tests/integration/dbclient/pgsql/etc/docker/Dockerfile index f0778bb6852..587335551ec 100644 --- a/tests/integration/dbclient/pgsql/etc/docker/Dockerfile +++ b/tests/integration/dbclient/pgsql/etc/docker/Dockerfile @@ -17,8 +17,9 @@ FROM oraclelinux:9-slim RUN microdnf install postgresql-server && microdnf clean all +RUN mkdir -p /var/run/postgresql && \ + chown postgres:postgres /var/run/postgresql ADD entrypoint.sh /usr/local/bin/ - ENV PGDATA /var/lib/pgsql/data USER postgres ENTRYPOINT ["entrypoint.sh"] diff --git a/tests/integration/dbclient/pgsql/pom.xml b/tests/integration/dbclient/pgsql/pom.xml index 63f11281e70..d7f365ee9d3 100644 --- a/tests/integration/dbclient/pgsql/pom.xml +++ b/tests/integration/dbclient/pgsql/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration.dbclient helidon-tests-integration-dbclient-parent - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../parent/pom.xml helidon-tests-integration-dbclient-pgsql diff --git a/tests/integration/dbclient/pom.xml b/tests/integration/dbclient/pom.xml index 3806838afeb..fb92f145a15 100644 --- a/tests/integration/dbclient/pom.xml +++ b/tests/integration/dbclient/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.integration.dbclient helidon-tests-integration-dbclient-project diff --git a/tests/integration/harness/pom.xml b/tests/integration/harness/pom.xml index 109c34b5b7b..839c63fcc16 100644 --- a/tests/integration/harness/pom.xml +++ b/tests/integration/harness/pom.xml @@ -21,7 +21,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-harness Helidon Tests Integration Harness diff --git a/tests/integration/health/mp-disabled/pom.xml b/tests/integration/health/mp-disabled/pom.xml index 1507c478c44..2879f71356f 100644 --- a/tests/integration/health/mp-disabled/pom.xml +++ b/tests/integration/health/mp-disabled/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-mp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../../applications/mp/pom.xml io.helidon.tests.integration.health diff --git a/tests/integration/health/pom.xml b/tests/integration/health/pom.xml index 88af4cf1140..1a0f15c3093 100644 --- a/tests/integration/health/pom.xml +++ b/tests/integration/health/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.integration.health helidon-tests-integration-health-project diff --git a/tests/integration/jep290/check_f_f_ok/pom.xml b/tests/integration/jep290/check_f_f_ok/pom.xml index 643232b0cb6..7459bef9fbb 100644 --- a/tests/integration/jep290/check_f_f_ok/pom.xml +++ b/tests/integration/jep290/check_f_f_ok/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.jep290 helidon-tests-integration-jep290-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-jep290-check_f_f_ok diff --git a/tests/integration/jep290/check_f_f_w/pom.xml b/tests/integration/jep290/check_f_f_w/pom.xml index 1c3e2f5a85a..32d667b6949 100644 --- a/tests/integration/jep290/check_f_f_w/pom.xml +++ b/tests/integration/jep290/check_f_f_w/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.jep290 helidon-tests-integration-jep290-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-jep290-check_f_f_w diff --git a/tests/integration/jep290/check_f_p_ok/pom.xml b/tests/integration/jep290/check_f_p_ok/pom.xml index 4a98bf1ef2d..e065da13342 100644 --- a/tests/integration/jep290/check_f_p_ok/pom.xml +++ b/tests/integration/jep290/check_f_p_ok/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.jep290 helidon-tests-integration-jep290-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-jep290-check_f_p_ok diff --git a/tests/integration/jep290/check_f_p_w/pom.xml b/tests/integration/jep290/check_f_p_w/pom.xml index a5cf3c15f6c..b8973689762 100644 --- a/tests/integration/jep290/check_f_p_w/pom.xml +++ b/tests/integration/jep290/check_f_p_w/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.jep290 helidon-tests-integration-jep290-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-jep290-check_f_p_w diff --git a/tests/integration/jep290/pom.xml b/tests/integration/jep290/pom.xml index 8962ffc2808..835796a0465 100644 --- a/tests/integration/jep290/pom.xml +++ b/tests/integration/jep290/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.integration.jep290 helidon-tests-integration-jep290-project diff --git a/tests/integration/jep290/server_and_custom/pom.xml b/tests/integration/jep290/server_and_custom/pom.xml index 12cc6f0e677..e94c6df33ec 100644 --- a/tests/integration/jep290/server_and_custom/pom.xml +++ b/tests/integration/jep290/server_and_custom/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration.jep290 helidon-tests-integration-jep290-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-jep290-server-and-custom diff --git a/tests/integration/jep290/set_c_f_c/pom.xml b/tests/integration/jep290/set_c_f_c/pom.xml index 5f4fe19cd29..c696ff80d47 100644 --- a/tests/integration/jep290/set_c_f_c/pom.xml +++ b/tests/integration/jep290/set_c_f_c/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.jep290 helidon-tests-integration-jep290-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-jep290-set_c_f_c diff --git a/tests/integration/jep290/set_c_f_d/pom.xml b/tests/integration/jep290/set_c_f_d/pom.xml index 21a6f0a86f9..dd8485441f0 100644 --- a/tests/integration/jep290/set_c_f_d/pom.xml +++ b/tests/integration/jep290/set_c_f_d/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.jep290 helidon-tests-integration-jep290-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-jep290-set_c_f_d diff --git a/tests/integration/jep290/set_c_t_d/pom.xml b/tests/integration/jep290/set_c_t_d/pom.xml index 26b3fe28567..5662dae6fc5 100644 --- a/tests/integration/jep290/set_c_t_d/pom.xml +++ b/tests/integration/jep290/set_c_t_d/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.jep290 helidon-tests-integration-jep290-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-jep290-set_c_t_d diff --git a/tests/integration/jep290/set_f/pom.xml b/tests/integration/jep290/set_f/pom.xml index 3a49deb0bd2..4bd2a157d05 100644 --- a/tests/integration/jep290/set_f/pom.xml +++ b/tests/integration/jep290/set_f/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.jep290 helidon-tests-integration-jep290-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-jep290-set_f diff --git a/tests/integration/jep290/set_o/pom.xml b/tests/integration/jep290/set_o/pom.xml index 6eb3b00c51a..b8cdd63170f 100644 --- a/tests/integration/jep290/set_o/pom.xml +++ b/tests/integration/jep290/set_o/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.jep290 helidon-tests-integration-jep290-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-jep290-set_o diff --git a/tests/integration/jms/pom.xml b/tests/integration/jms/pom.xml index 20901b05f7b..2530681ed3e 100644 --- a/tests/integration/jms/pom.xml +++ b/tests/integration/jms/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.integration.jms diff --git a/tests/integration/jpa/appl/pom.xml b/tests/integration/jpa/appl/pom.xml index 7c6c69c2805..0cd004c0790 100644 --- a/tests/integration/jpa/appl/pom.xml +++ b/tests/integration/jpa/appl/pom.xml @@ -23,7 +23,7 @@ io.helidon.applications helidon-mp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../../applications/mp/pom.xml diff --git a/tests/integration/jpa/model/pom.xml b/tests/integration/jpa/model/pom.xml index d66dbb669b7..39c7204c7c0 100644 --- a/tests/integration/jpa/model/pom.xml +++ b/tests/integration/jpa/model/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.jpa helidon-tests-integration-jpa-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.integration.jpa diff --git a/tests/integration/jpa/pom.xml b/tests/integration/jpa/pom.xml index 70aa3320122..da491cc7ffd 100644 --- a/tests/integration/jpa/pom.xml +++ b/tests/integration/jpa/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/tests/integration/jpa/simple/pom.xml b/tests/integration/jpa/simple/pom.xml index e39bfd5b877..29ddb2abfdd 100644 --- a/tests/integration/jpa/simple/pom.xml +++ b/tests/integration/jpa/simple/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration.jpa helidon-tests-integration-jpa-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-jpa-simple diff --git a/tests/integration/kafka/pom.xml b/tests/integration/kafka/pom.xml index fc2499b4731..b12491a0b4d 100644 --- a/tests/integration/kafka/pom.xml +++ b/tests/integration/kafka/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.integration.kafka diff --git a/tests/integration/mp-bean-validation/pom.xml b/tests/integration/mp-bean-validation/pom.xml index 1745ac6f824..e2b61a09e47 100644 --- a/tests/integration/mp-bean-validation/pom.xml +++ b/tests/integration/mp-bean-validation/pom.xml @@ -24,7 +24,7 @@ io.helidon.applications helidon-mp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../applications/mp/pom.xml io.helidon.tests.integration diff --git a/tests/integration/mp-gh-2421/pom.xml b/tests/integration/mp-gh-2421/pom.xml index f7d0513a142..59d901960cd 100644 --- a/tests/integration/mp-gh-2421/pom.xml +++ b/tests/integration/mp-gh-2421/pom.xml @@ -22,7 +22,7 @@ helidon-tests-integration-project io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/mp-gh-2461/pom.xml b/tests/integration/mp-gh-2461/pom.xml index 320880bef87..5bf7dec9fd1 100644 --- a/tests/integration/mp-gh-2461/pom.xml +++ b/tests/integration/mp-gh-2461/pom.xml @@ -22,7 +22,7 @@ helidon-tests-integration-project io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/mp-gh-3246/pom.xml b/tests/integration/mp-gh-3246/pom.xml index 7f68962da97..ea32512b090 100644 --- a/tests/integration/mp-gh-3246/pom.xml +++ b/tests/integration/mp-gh-3246/pom.xml @@ -22,7 +22,7 @@ helidon-tests-integration-project io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/mp-gh-3974/pom.xml b/tests/integration/mp-gh-3974/pom.xml index 87e6175b835..1ff666fe357 100644 --- a/tests/integration/mp-gh-3974/pom.xml +++ b/tests/integration/mp-gh-3974/pom.xml @@ -22,7 +22,7 @@ helidon-tests-integration-project io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/mp-gh-4123/pom.xml b/tests/integration/mp-gh-4123/pom.xml index b4a7ba33c8d..520a24e2f11 100644 --- a/tests/integration/mp-gh-4123/pom.xml +++ b/tests/integration/mp-gh-4123/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/mp-gh-4654/pom.xml b/tests/integration/mp-gh-4654/pom.xml index 40f47ef07cd..50fb9a9ed37 100644 --- a/tests/integration/mp-gh-4654/pom.xml +++ b/tests/integration/mp-gh-4654/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/mp-gh-5328/pom.xml b/tests/integration/mp-gh-5328/pom.xml index 73771b36c3b..34a18767f7d 100644 --- a/tests/integration/mp-gh-5328/pom.xml +++ b/tests/integration/mp-gh-5328/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/mp-gh-8349/pom.xml b/tests/integration/mp-gh-8349/pom.xml index fc77821940f..3fad987f473 100644 --- a/tests/integration/mp-gh-8349/pom.xml +++ b/tests/integration/mp-gh-8349/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/mp-gh-8478/pom.xml b/tests/integration/mp-gh-8478/pom.xml index edbbf70ac40..59350f2290e 100644 --- a/tests/integration/mp-gh-8478/pom.xml +++ b/tests/integration/mp-gh-8478/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/mp-gh-8493/pom.xml b/tests/integration/mp-gh-8493/pom.xml index 64eeaeff943..0bd5cdab4d1 100644 --- a/tests/integration/mp-gh-8493/pom.xml +++ b/tests/integration/mp-gh-8493/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/mp-gh-8495/pom.xml b/tests/integration/mp-gh-8495/pom.xml index d9b9512743a..1cb205f4806 100644 --- a/tests/integration/mp-gh-8495/pom.xml +++ b/tests/integration/mp-gh-8495/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/mp-graphql/pom.xml b/tests/integration/mp-graphql/pom.xml index 5ff5e2db8b7..4adfd967b69 100644 --- a/tests/integration/mp-graphql/pom.xml +++ b/tests/integration/mp-graphql/pom.xml @@ -21,7 +21,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-mp-graphql diff --git a/tests/integration/mp-security-client/pom.xml b/tests/integration/mp-security-client/pom.xml index e7bbe2fc62f..a28881e8e9d 100644 --- a/tests/integration/mp-security-client/pom.xml +++ b/tests/integration/mp-security-client/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-mp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../applications/mp/pom.xml io.helidon.tests.integration diff --git a/tests/integration/mp-ws-services/pom.xml b/tests/integration/mp-ws-services/pom.xml index 8d64fbe4135..6044c6ae3e9 100644 --- a/tests/integration/mp-ws-services/pom.xml +++ b/tests/integration/mp-ws-services/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-mp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../applications/mp/pom.xml io.helidon.tests.integration diff --git a/tests/integration/oidc/pom.xml b/tests/integration/oidc/pom.xml index 6b7e37a216c..dc08579de1b 100644 --- a/tests/integration/oidc/pom.xml +++ b/tests/integration/oidc/pom.xml @@ -21,7 +21,7 @@ helidon-tests-integration-project io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/packaging/mp-1/pom.xml b/tests/integration/packaging/mp-1/pom.xml index c499ce8f17b..9053497177b 100644 --- a/tests/integration/packaging/mp-1/pom.xml +++ b/tests/integration/packaging/mp-1/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-mp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../../applications/mp/pom.xml io.helidon.tests.integration.packaging diff --git a/tests/integration/packaging/mp-2/pom.xml b/tests/integration/packaging/mp-2/pom.xml index d26f8644a8f..be51c3b9827 100644 --- a/tests/integration/packaging/mp-2/pom.xml +++ b/tests/integration/packaging/mp-2/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-mp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../../applications/mp/pom.xml io.helidon.tests.integration.packaging diff --git a/tests/integration/packaging/mp-3/pom.xml b/tests/integration/packaging/mp-3/pom.xml index f6383cc74d6..b12400f384d 100644 --- a/tests/integration/packaging/mp-3/pom.xml +++ b/tests/integration/packaging/mp-3/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-mp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../../applications/mp/pom.xml io.helidon.tests.integration.packaging diff --git a/tests/integration/packaging/pom.xml b/tests/integration/packaging/pom.xml index 461d5928a4a..04b8081e3ae 100644 --- a/tests/integration/packaging/pom.xml +++ b/tests/integration/packaging/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.tests.integration.packaging diff --git a/tests/integration/packaging/se-1/pom.xml b/tests/integration/packaging/se-1/pom.xml index 9e942340afd..7b759abcf76 100644 --- a/tests/integration/packaging/se-1/pom.xml +++ b/tests/integration/packaging/se-1/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-se - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../../applications/se/pom.xml io.helidon.tests.integration.packaging diff --git a/tests/integration/packaging/se-1/src/main/resources/logging.properties b/tests/integration/packaging/se-1/src/main/resources/logging.properties index 48878bf93d9..c19a3babf9c 100644 --- a/tests/integration/packaging/se-1/src/main/resources/logging.properties +++ b/tests/integration/packaging/se-1/src/main/resources/logging.properties @@ -19,18 +19,11 @@ # Send messages to the console handlers=io.helidon.logging.jul.HelidonConsoleHandler - -# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread -java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n +# Use JSON formatter with default format +io.helidon.logging.jul.HelidonConsoleHandler.formatter=io.helidon.logging.jul.HelidonJsonFormatter # Global logging level. Can be overridden by specific loggers .level=INFO -# Component specific log levels -#io.helidon.webserver.level=INFO -#io.helidon.config.level=INFO -#io.helidon.security.level=INFO -#io.helidon.common.level=INFO - io.helidon.webserver.staticcontent.ClassPathContentHandler.level=FINEST io.helidon.webserver.staticcontent.FileSystemContentHandler.level=FINEST diff --git a/tests/integration/packaging/static-content/pom.xml b/tests/integration/packaging/static-content/pom.xml index 66293c0042e..70e445d5af6 100644 --- a/tests/integration/packaging/static-content/pom.xml +++ b/tests/integration/packaging/static-content/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration.packaging helidon-tests-integration-packaging - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-packaging-static-content Helidon Tests Integration Packaging Static Content diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml index d29401cf66f..aaf2d127b53 100644 --- a/tests/integration/pom.xml +++ b/tests/integration/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests helidon-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.integration helidon-tests-integration-project diff --git a/tests/integration/restclient-connector/pom.xml b/tests/integration/restclient-connector/pom.xml index f1bc2e369aa..f36cca4f12b 100644 --- a/tests/integration/restclient-connector/pom.xml +++ b/tests/integration/restclient-connector/pom.xml @@ -22,7 +22,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-restclient-connector diff --git a/tests/integration/restclient/pom.xml b/tests/integration/restclient/pom.xml index ddf62de58c0..76bca01d835 100644 --- a/tests/integration/restclient/pom.xml +++ b/tests/integration/restclient/pom.xml @@ -21,7 +21,7 @@ helidon-tests-integration-project io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/security/abac-policy/pom.xml b/tests/integration/security/abac-policy/pom.xml new file mode 100644 index 00000000000..661dececfe3 --- /dev/null +++ b/tests/integration/security/abac-policy/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + io.helidon.tests.integration + helidon-tests-integration-security + 4.2.0-SNAPSHOT + + + helidon-tests-integration-security-abac-policy + Helidon Tests Integration ABAC Policy Validator + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.security.abac + helidon-security-abac-policy-el + + + org.glassfish + jakarta.el + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + + \ No newline at end of file diff --git a/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementExplicitResource.java b/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementExplicitResource.java new file mode 100644 index 00000000000..c470f7ae02c --- /dev/null +++ b/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementExplicitResource.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.abac.policy; + +import io.helidon.security.AuthorizationResponse; +import io.helidon.security.SecurityContext; +import io.helidon.security.abac.policy.PolicyValidator; +import io.helidon.security.annotations.Authorized; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; + +/** + * A resource with explicit authorization. + */ +@Path("/explicit") +public class PolicyStatementExplicitResource { + + /** + * Policy statement and explicit authorization is set via annotations and not overridden. + * + * @param context security context to perform an explicit authorization + * @return result of the authorization + */ + @GET + @Path("annotation") + @Authorized(explicit = true) + @PolicyValidator.PolicyStatement("${env.time.year >= 2017}") + public Response annotation(@Context SecurityContext context) { + AuthorizationResponse atzResponse = context.authorize(); + + if (atzResponse.isPermitted()) { + return Response.ok().entity("passed").build(); + } else { + return Response.status(Response.Status.FORBIDDEN) + .entity(atzResponse.description().orElse("Access not granted")) + .build(); + } + } + + /** + * Policy statement and explicit authorization is set via configuration. + * + * @param context security context to perform an explicit authorization + * @return result of the authorization + */ + @GET + @Path("configuration") + @Authorized + @PolicyValidator.PolicyStatement("${env.time.year < 2017}") + public Response configuration(@Context SecurityContext context) { + AuthorizationResponse atzResponse = context.authorize(); + + if (atzResponse.isPermitted()) { + return Response.ok().entity("passed").build(); + } else { + return Response.status(Response.Status.FORBIDDEN) + .entity(atzResponse.description().orElse("Access not granted")) + .build(); + } + } + +} diff --git a/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementResource.java b/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementResource.java new file mode 100644 index 00000000000..1b3551b90db --- /dev/null +++ b/tests/integration/security/abac-policy/src/main/java/io/helidon/tests/integration/security/abac/policy/PolicyStatementResource.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.abac.policy; + +import io.helidon.security.abac.policy.PolicyValidator; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +/** + * A resource with abac policy statements. + */ +@Path("/policy") +public class PolicyStatementResource { + + /** + * Policy statement configured via annotation. + * + * @return passed value + */ + @GET + @Path("/annotation") + @PolicyValidator.PolicyStatement("${env.time.year >= 2017}") + public String annotation() { + return "passed"; + } + + /** + * Policy statement overridden by the config. + * + * @return passed value + */ + @GET + @Path("/override") + @PolicyValidator.PolicyStatement("${env.time.year <= 2017}") + public String override() { + return "passed"; + } + + /** + * Policy statement should not be overridden by the config. + * + * @return passed value + */ + @GET + @Path("/override2") + @PolicyValidator.PolicyStatement("${env.time.year <= 2017}") + public String override2() { + return "should not pass"; + } + + /** + * Policy statement overridden by the config with asterisk present in path. + * + * @return passed value + */ + @GET + @Path("/asterisk") + @PolicyValidator.PolicyStatement("${env.time.year <= 2017}") + public String asterisk() { + return "passed"; + } + + /** + * Policy statement overridden by the config with asterisk present in path. + * + * @return passed value + */ + @GET + @Path("/asterisk2") + @PolicyValidator.PolicyStatement("${env.time.year <= 2017}") + public String asterisk2() { + return "passed"; + } + + /** + * Policy statement not overridden by configuration and should not let anyone in. + * + * @return should not pass value + */ + @GET + @Path("/notOverride") + @PolicyValidator.PolicyStatement("${env.time.year <= 2017}") + public String notOverride() { + return "should not pass"; + } + +} diff --git a/tests/integration/security/abac-policy/src/main/resources/META-INF/beans.xml b/tests/integration/security/abac-policy/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..52f89a20d18 --- /dev/null +++ b/tests/integration/security/abac-policy/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/tests/integration/security/abac-policy/src/main/resources/application.yaml b/tests/integration/security/abac-policy/src/main/resources/application.yaml new file mode 100644 index 00000000000..b3be701fd6a --- /dev/null +++ b/tests/integration/security/abac-policy/src/main/resources/application.yaml @@ -0,0 +1,35 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +server: + port: 0 + features: + security: + endpoints: + - path: "/policy/override" + config: + abac.policy-validator.statement: "${env.time.year >= 2017}" + - path: "/policy/asterisk*" + config: + abac.policy-validator.statement: "${env.time.year >= 2017}" + - path: "/explicit/configuration" + config: + authorize: true + authorization-explicit: true + abac.policy-validator.statement: "${env.time.year >= 2017}" + +security: + providers: + - abac: \ No newline at end of file diff --git a/tests/integration/security/abac-policy/src/main/resources/logging.properties b/tests/integration/security/abac-policy/src/main/resources/logging.properties new file mode 100644 index 00000000000..2a6f3a7be2a --- /dev/null +++ b/tests/integration/security/abac-policy/src/main/resources/logging.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + diff --git a/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/ExplicitPolicyTest.java b/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/ExplicitPolicyTest.java new file mode 100644 index 00000000000..a77ab2618ab --- /dev/null +++ b/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/ExplicitPolicyTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.abac.policy; + +import io.helidon.http.Status; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +class ExplicitPolicyTest { + + @Test + void testAnnotation(WebTarget target) { + try (Response response = target.path("/explicit/annotation") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + } + + @Test + void testConfiguration(WebTarget target) { + try (Response response = target.path("/explicit/configuration") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + } + +} diff --git a/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/PolicyTest.java b/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/PolicyTest.java new file mode 100644 index 00000000000..bf63cc4b70e --- /dev/null +++ b/tests/integration/security/abac-policy/src/test/java/io/helidon/tests/integration/security/abac/policy/PolicyTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.tests.integration.security.abac.policy; + +import io.helidon.http.Status; +import io.helidon.microprofile.testing.junit5.AddConfig; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +class PolicyTest { + + @Test + void testAnnotation(WebTarget target) { + try (Response response = target.path("/policy/annotation") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + } + + @Test + void testExplicitOverride(WebTarget target) { + try (Response response = target.path("/policy/override") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + try (Response response = target.path("/policy/override2") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.FORBIDDEN_403.code())); + } + } + + @Test + void testAsteriskOverride(WebTarget target) { + try (Response response = target.path("/policy/asterisk") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + try (Response response = target.path("/policy/asterisk2") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.OK_200.code())); + assertThat(response.readEntity(String.class), is("passed")); + } + } + + @Test + void testNotOverride(WebTarget target) { + try (Response response = target.path("/policy/notOverride") + .request() + .get()) { + assertThat(response.getStatus(), is(Status.FORBIDDEN_403.code())); + } + } +} diff --git a/tests/integration/security/gh1487/pom.xml b/tests/integration/security/gh1487/pom.xml index e4d2997f64b..ed911f1ea97 100644 --- a/tests/integration/security/gh1487/pom.xml +++ b/tests/integration/security/gh1487/pom.xml @@ -21,7 +21,7 @@ helidon-tests-integration-security io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/security/gh2297/pom.xml b/tests/integration/security/gh2297/pom.xml index 45feb6d3743..6bf6788e6bc 100644 --- a/tests/integration/security/gh2297/pom.xml +++ b/tests/integration/security/gh2297/pom.xml @@ -21,7 +21,7 @@ helidon-tests-integration-security io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/security/gh2455/pom.xml b/tests/integration/security/gh2455/pom.xml index 986da2f83b8..efa4681cd1e 100644 --- a/tests/integration/security/gh2455/pom.xml +++ b/tests/integration/security/gh2455/pom.xml @@ -21,7 +21,7 @@ helidon-tests-integration-security io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/security/gh2772/pom.xml b/tests/integration/security/gh2772/pom.xml index 2836a2518e2..f7437ba34c3 100644 --- a/tests/integration/security/gh2772/pom.xml +++ b/tests/integration/security/gh2772/pom.xml @@ -21,7 +21,7 @@ helidon-tests-integration-security io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/security/path-params/pom.xml b/tests/integration/security/path-params/pom.xml index 1dabe3f5406..c5f9b434cac 100644 --- a/tests/integration/security/path-params/pom.xml +++ b/tests/integration/security/path-params/pom.xml @@ -22,7 +22,7 @@ helidon-tests-integration-security io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/security/pom.xml b/tests/integration/security/pom.xml index 259b82483c7..f6d6313f3ef 100644 --- a/tests/integration/security/pom.xml +++ b/tests/integration/security/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom @@ -42,5 +42,6 @@ path-params security-response-mapper security-annotation + abac-policy diff --git a/tests/integration/security/security-annotation/pom.xml b/tests/integration/security/security-annotation/pom.xml index a5af6a875a8..0b2f4244dda 100644 --- a/tests/integration/security/security-annotation/pom.xml +++ b/tests/integration/security/security-annotation/pom.xml @@ -21,7 +21,7 @@ io.helidon.tests.integration helidon-tests-integration-security - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT diff --git a/tests/integration/security/security-response-mapper/pom.xml b/tests/integration/security/security-response-mapper/pom.xml index 6091c8d371e..d6133ac5904 100644 --- a/tests/integration/security/security-response-mapper/pom.xml +++ b/tests/integration/security/security-response-mapper/pom.xml @@ -21,7 +21,7 @@ helidon-tests-integration-security io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/tests/integration/tls-revocation-config/pom.xml b/tests/integration/tls-revocation-config/pom.xml index 26eef2d67b8..68eff6d6cf2 100644 --- a/tests/integration/tls-revocation-config/pom.xml +++ b/tests/integration/tls-revocation-config/pom.xml @@ -21,7 +21,7 @@ io.helidon.tests.integration helidon-tests-integration-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-integration-tls-revocation-config diff --git a/tests/integration/vault/pom.xml b/tests/integration/vault/pom.xml index dc9faec973a..3047602b3fd 100644 --- a/tests/integration/vault/pom.xml +++ b/tests/integration/vault/pom.xml @@ -23,7 +23,7 @@ helidon-tests-integration-project io.helidon.tests.integration - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests.integration.vault diff --git a/tests/integration/zipkin-mp-2.2/pom.xml b/tests/integration/zipkin-mp-2.2/pom.xml index 99b8e6183d1..79c150b6ce4 100644 --- a/tests/integration/zipkin-mp-2.2/pom.xml +++ b/tests/integration/zipkin-mp-2.2/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-mp - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT ../../../applications/mp/pom.xml io.helidon.tests.integration diff --git a/tests/pom.xml b/tests/pom.xml index 7dffe1b5c74..bbccbcca873 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -23,7 +23,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.tests diff --git a/tests/tck/pom.xml b/tests/tck/pom.xml index 73b3d07505d..4b6c8e31b10 100644 --- a/tests/tck/pom.xml +++ b/tests/tck/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests helidon-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 pom diff --git a/tests/tck/tck-reactive-streams/pom.xml b/tests/tck/tck-reactive-streams/pom.xml index 6d7d070a9e7..c1033af7165 100644 --- a/tests/tck/tck-reactive-streams/pom.xml +++ b/tests/tck/tck-reactive-streams/pom.xml @@ -23,7 +23,7 @@ io.helidon.tests tck-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 tck-reactive-streams diff --git a/tracing/config/pom.xml b/tracing/config/pom.xml index 61396dc214c..0922aa94363 100644 --- a/tracing/config/pom.xml +++ b/tracing/config/pom.xml @@ -22,7 +22,7 @@ io.helidon.tracing helidon-tracing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tracing-config diff --git a/tracing/jersey-client/pom.xml b/tracing/jersey-client/pom.xml index f21a97da106..d0c18b0a08b 100644 --- a/tracing/jersey-client/pom.xml +++ b/tracing/jersey-client/pom.xml @@ -22,7 +22,7 @@ io.helidon.tracing helidon-tracing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tracing-jersey-client diff --git a/tracing/jersey/pom.xml b/tracing/jersey/pom.xml index 9360ebe8fd1..29daeb3697b 100644 --- a/tracing/jersey/pom.xml +++ b/tracing/jersey/pom.xml @@ -22,7 +22,7 @@ io.helidon.tracing helidon-tracing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tracing-jersey diff --git a/tracing/pom.xml b/tracing/pom.xml index c22c81e2812..da42c8fc106 100644 --- a/tracing/pom.xml +++ b/tracing/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.tracing diff --git a/tracing/provider-tests/pom.xml b/tracing/provider-tests/pom.xml index 0344d71e8f2..f976d19c20b 100644 --- a/tracing/provider-tests/pom.xml +++ b/tracing/provider-tests/pom.xml @@ -21,7 +21,7 @@ io.helidon.tracing helidon-tracing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tracing-provider-tests diff --git a/tracing/providers/jaeger/pom.xml b/tracing/providers/jaeger/pom.xml index 731cb3709a3..1768d7baec7 100644 --- a/tracing/providers/jaeger/pom.xml +++ b/tracing/providers/jaeger/pom.xml @@ -22,7 +22,7 @@ io.helidon.tracing.providers helidon-tracing-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tracing-providers-jaeger diff --git a/tracing/providers/opentelemetry/pom.xml b/tracing/providers/opentelemetry/pom.xml index d8bdf95fa07..d3b1bde9b37 100644 --- a/tracing/providers/opentelemetry/pom.xml +++ b/tracing/providers/opentelemetry/pom.xml @@ -21,7 +21,7 @@ io.helidon.tracing.providers helidon-tracing-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tracing-providers-opentelemetry diff --git a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryDataPropagationProvider.java b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryDataPropagationProvider.java index 014c1283c32..1705dc40ea7 100644 --- a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryDataPropagationProvider.java +++ b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryDataPropagationProvider.java @@ -62,7 +62,7 @@ public void propagateData(OpenTelemetryContext context) { } /** - * OpenTelementry context. + * OpenTelemetry context. */ public static class OpenTelemetryContext { private final Span span; diff --git a/tracing/providers/opentracing/pom.xml b/tracing/providers/opentracing/pom.xml index 48f08bf14d9..4feeb428db3 100644 --- a/tracing/providers/opentracing/pom.xml +++ b/tracing/providers/opentracing/pom.xml @@ -21,7 +21,7 @@ io.helidon.tracing.providers helidon-tracing-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tracing-providers-opentracing diff --git a/tracing/providers/opentracing/src/main/java/io/helidon/tracing/providers/opentracing/OpenTracingTracerProvider.java b/tracing/providers/opentracing/src/main/java/io/helidon/tracing/providers/opentracing/OpenTracingTracerProvider.java index 2926fa3f385..c3c887be51e 100644 --- a/tracing/providers/opentracing/src/main/java/io/helidon/tracing/providers/opentracing/OpenTracingTracerProvider.java +++ b/tracing/providers/opentracing/src/main/java/io/helidon/tracing/providers/opentracing/OpenTracingTracerProvider.java @@ -23,6 +23,8 @@ import io.helidon.common.LazyValue; import io.helidon.common.Weight; import io.helidon.common.Weighted; +import io.helidon.common.config.Config; +import io.helidon.common.config.GlobalConfig; import io.helidon.tracing.Span; import io.helidon.tracing.SpanListener; import io.helidon.tracing.Tracer; @@ -35,6 +37,10 @@ /** * {@link java.util.ServiceLoader} service implementation of {@link io.helidon.tracing.spi.TracerProvider} for Open Tracing * tracers. + *

+ * When dealing with the global tracer, manage both the Helidon one and also the OpenTracing one in concert, whether + * defaulting them or assigning them via {@link #global(io.helidon.tracing.Tracer)}. + *

*/ @Weight(Weighted.DEFAULT_WEIGHT - 50) // low weight, so it is easy to override public class OpenTracingTracerProvider implements TracerProvider { @@ -42,6 +48,19 @@ public class OpenTracingTracerProvider implements TracerProvider { private static final LazyValue> SPAN_LISTENERS = LazyValue.create(() -> HelidonServiceLoader.create(ServiceLoader.load(SpanListener.class)).asList()); + private LazyValue globalHelidonTracer = LazyValue.create(() -> { + Config tracingConfig = GlobalConfig.config().get("tracing"); + + // Set up to create an explicit OpenTracing tracer only if we have config for tracing, indicating that the user wants + // something other than the no-op implementation. + if (tracingConfig.exists()) { + io.opentracing.Tracer openTracingTracer = OpenTracingTracerBuilder.create(tracingConfig) + .build(); + GlobalTracer.registerIfAbsent(openTracingTracer); + } + return OpenTracingTracer.create(GlobalTracer.get()); + }); + @Override public TracerBuilder createBuilder() { return OpenTracingTracer.builder(); @@ -49,13 +68,17 @@ public TracerBuilder createBuilder() { @Override public Tracer global() { - return OpenTracingTracer.create(GlobalTracer.get()); + return globalHelidonTracer.get(); } @Override public void global(Tracer tracer) { if (tracer instanceof OpenTracingTracer opt) { - GlobalTracer.registerIfAbsent(opt.openTracing()); + GlobalTracer.registerIfAbsent(() -> { + io.opentracing.Tracer openTracingTracer = opt.openTracing(); + globalHelidonTracer = LazyValue.create(OpenTracingTracer.create(openTracingTracer)); + return openTracingTracer; + }); } } diff --git a/tracing/providers/pom.xml b/tracing/providers/pom.xml index 0d3d4997bde..7a605dba855 100644 --- a/tracing/providers/pom.xml +++ b/tracing/providers/pom.xml @@ -22,7 +22,7 @@ io.helidon.tracing helidon-tracing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom io.helidon.tracing.providers diff --git a/tracing/providers/zipkin/pom.xml b/tracing/providers/zipkin/pom.xml index 26ca50caa6a..6073a49b77a 100644 --- a/tracing/providers/zipkin/pom.xml +++ b/tracing/providers/zipkin/pom.xml @@ -22,7 +22,7 @@ io.helidon.tracing.providers helidon-tracing-providers-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tracing-providers-zipkin @@ -57,6 +57,10 @@ io.opentracing.brave brave-opentracing
+ + io.helidon.common + helidon-common-context + io.helidon.common.features helidon-common-features-api @@ -138,17 +142,33 @@ org.apache.maven.plugins maven-surefire-plugin - - - io.helidon.tracing.providers.tests.TestTracerAndSpanPropagation.java - - + + + default-test + + + io.helidon.tracing.providers.tests.TestTracerAndSpanPropagation.java + + + + + zipkin-propagation-test + + test + + + + io.helidon.tracing.providers.tests.TestTracerAndSpanPropagation.java + + + + diff --git a/tracing/providers/zipkin/src/main/java/io/helidon/tracing/providers/zipkin/ZipkinDataPropagationProvider.java b/tracing/providers/zipkin/src/main/java/io/helidon/tracing/providers/zipkin/ZipkinDataPropagationProvider.java new file mode 100644 index 00000000000..123d32fb83b --- /dev/null +++ b/tracing/providers/zipkin/src/main/java/io/helidon/tracing/providers/zipkin/ZipkinDataPropagationProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.tracing.providers.zipkin; + +import io.helidon.common.context.Contexts; +import io.helidon.common.context.spi.DataPropagationProvider; + +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; + +/** + * Data propagation provider for the Helidon Zipkin tracing provider. + */ +public class ZipkinDataPropagationProvider implements DataPropagationProvider { + + private static final System.Logger LOGGER = System.getLogger(ZipkinDataPropagationProvider.class.getName()); + + /** + * Creates new provider; public for service loading. + */ + public ZipkinDataPropagationProvider() { + } + + @Override + public ZipkinContext data() { + Tracer tracer = Contexts.context() + .flatMap(ctx -> ctx.get(Tracer.class)) + .orElseGet(GlobalTracer::get); + Span span = tracer.activeSpan(); + return new ZipkinContext(tracer, span); + } + + @Override + public void propagateData(ZipkinContext data) { + if (data != null && data.span != null) { + data.scope = data.tracer.activateSpan(data.span); + } + } + + @Override + public void clearData(ZipkinContext data) { + if (data != null && data.scope != null) { + try { + data.scope.close(); + } catch (Exception e) { + LOGGER.log(System.Logger.Level.TRACE, "Cannot close tracing span", e); + } + } + } + + /** + * Zipkin-specific propagation context. + */ + static class ZipkinContext { + + private final Tracer tracer; + private final Span span; + private Scope scope; + + private ZipkinContext(Tracer tracer, Span span) { + this.tracer = tracer; + this.span = span; + } + } +} diff --git a/tracing/providers/zipkin/src/main/java/io/helidon/tracing/providers/zipkin/ZipkinTracerProvider.java b/tracing/providers/zipkin/src/main/java/io/helidon/tracing/providers/zipkin/ZipkinTracerProvider.java index 2077f3901bf..fcdc8f2e816 100644 --- a/tracing/providers/zipkin/src/main/java/io/helidon/tracing/providers/zipkin/ZipkinTracerProvider.java +++ b/tracing/providers/zipkin/src/main/java/io/helidon/tracing/providers/zipkin/ZipkinTracerProvider.java @@ -48,6 +48,12 @@ public class ZipkinTracerProvider implements OpenTracingProvider { private static final List TRACING_CONTEXT_PROPAGATION_HEADERS = List.of(X_OT_SPAN_CONTEXT, X_B3_TRACE_ID, X_B3_SPAN_ID, X_B3_PARENT_SPAN_ID, X_B3_SAMPLED, X_B3_FLAGS); + /** + * Public constructor for service loading. + */ + public ZipkinTracerProvider() { + } + @Override public ZipkinTracerBuilder createBuilder() { return ZipkinTracerBuilder.create(); diff --git a/tracing/providers/zipkin/src/main/java/module-info.java b/tracing/providers/zipkin/src/main/java/module-info.java index afc01904698..23d95ba0206 100644 --- a/tracing/providers/zipkin/src/main/java/module-info.java +++ b/tracing/providers/zipkin/src/main/java/module-info.java @@ -30,6 +30,7 @@ requires brave.opentracing; requires brave; requires io.helidon.common; + requires io.helidon.common.context; requires io.helidon.tracing.providers.opentracing; requires io.opentracing.noop; requires io.opentracing.util; @@ -51,4 +52,6 @@ provides io.helidon.tracing.providers.opentracing.spi.OpenTracingProvider with io.helidon.tracing.providers.zipkin.ZipkinTracerProvider; + provides io.helidon.common.context.spi.DataPropagationProvider + with io.helidon.tracing.providers.zipkin.ZipkinDataPropagationProvider; } \ No newline at end of file diff --git a/tracing/providers/zipkin/src/test/resources/application.yaml b/tracing/providers/zipkin/src/test/resources/application.yaml index 0f359e5fb7e..57a4e673af9 100644 --- a/tracing/providers/zipkin/src/test/resources/application.yaml +++ b/tracing/providers/zipkin/src/test/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2017, 2023 Oracle and/or its affiliates. +# Copyright (c) 2017, 2024 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -45,4 +45,5 @@ tracing: int-tags: tag5: 145 tag6: 741 - + # With changes to OpenTracing global tracer handling, provide a service name for the Zipkin implementation to use. + service: "helidon-test-service" diff --git a/tracing/tests/it-tracing-client-zipkin/pom.xml b/tracing/tests/it-tracing-client-zipkin/pom.xml index c96ae79683c..c7de2c92661 100644 --- a/tracing/tests/it-tracing-client-zipkin/pom.xml +++ b/tracing/tests/it-tracing-client-zipkin/pom.xml @@ -22,7 +22,7 @@ io.helidon.tracing helidon-tracing-tests - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tracing-tests-it1 diff --git a/tracing/tests/pom.xml b/tracing/tests/pom.xml index 78f85fd2361..80831fbd699 100644 --- a/tracing/tests/pom.xml +++ b/tracing/tests/pom.xml @@ -25,7 +25,7 @@ io.helidon.tracing helidon-tracing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/tracing/tracer-resolver/pom.xml b/tracing/tracer-resolver/pom.xml index 722e0029241..30d5941b504 100644 --- a/tracing/tracer-resolver/pom.xml +++ b/tracing/tracer-resolver/pom.xml @@ -22,7 +22,7 @@ io.helidon.tracing helidon-tracing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tracing-tracer-resolver diff --git a/tracing/tracing/pom.xml b/tracing/tracing/pom.xml index 6a0440a5081..7fcb650806b 100644 --- a/tracing/tracing/pom.xml +++ b/tracing/tracing/pom.xml @@ -22,7 +22,7 @@ io.helidon.tracing helidon-tracing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tracing diff --git a/webclient/api/pom.xml b/webclient/api/pom.xml index 4fbd24dff15..c4af5d2e73d 100644 --- a/webclient/api/pom.xml +++ b/webclient/api/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-api Helidon WebClient API diff --git a/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequestBase.java b/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequestBase.java index bdd3f053203..d1cec37df6e 100644 --- a/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequestBase.java +++ b/webclient/api/src/main/java/io/helidon/webclient/api/ClientRequestBase.java @@ -262,7 +262,7 @@ public R request() { } @Override - public final R submit(Object entity) { + public R submit(Object entity) { if (!(entity instanceof byte[] bytes && bytes.length == 0)) { rejectHeadWithEntity(); } @@ -271,7 +271,7 @@ public final R submit(Object entity) { } @Override - public final R outputStream(OutputStreamHandler outputStreamConsumer) { + public R outputStream(OutputStreamHandler outputStreamConsumer) { rejectHeadWithEntity(); additionalHeaders(); return doOutputStream(outputStreamConsumer); @@ -481,7 +481,7 @@ private String resolvePathParams(String path) { } private void rejectHeadWithEntity() { - if (this.method.equals(Method.HEAD)) { + if (Method.HEAD.equals(this.method)) { throw new IllegalArgumentException("Payload in method '" + Method.HEAD + "' has no defined semantics"); } } diff --git a/webclient/dns-resolver/first/pom.xml b/webclient/dns-resolver/first/pom.xml index e1ac0ee0398..f182b063682 100644 --- a/webclient/dns-resolver/first/pom.xml +++ b/webclient/dns-resolver/first/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient.dns.resolver helidon-webclient-dns-resolver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-dns-resolver-first diff --git a/webclient/dns-resolver/pom.xml b/webclient/dns-resolver/pom.xml index 53694a73527..1e229f827a1 100644 --- a/webclient/dns-resolver/pom.xml +++ b/webclient/dns-resolver/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT pom diff --git a/webclient/dns-resolver/round-robin/pom.xml b/webclient/dns-resolver/round-robin/pom.xml index ac887964cf2..478a5f401d6 100644 --- a/webclient/dns-resolver/round-robin/pom.xml +++ b/webclient/dns-resolver/round-robin/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient.dns.resolver helidon-webclient-dns-resolver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-dns-resolver-round-robin diff --git a/webclient/grpc/etc/spotbugs/exclude.xml b/webclient/grpc/etc/spotbugs/exclude.xml new file mode 100644 index 00000000000..2474da70a2c --- /dev/null +++ b/webclient/grpc/etc/spotbugs/exclude.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/webclient/grpc/pom.xml b/webclient/grpc/pom.xml index 5058e5c98cc..618e8559637 100644 --- a/webclient/grpc/pom.xml +++ b/webclient/grpc/pom.xml @@ -21,12 +21,16 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-grpc Helidon WebClient gRPC + + etc/spotbugs/exclude.xml + + io.grpc diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientUriSupplier.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientUriSupplier.java new file mode 100644 index 00000000000..f63693ec4a9 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientUriSupplier.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.webclient.grpc; + +import java.util.Iterator; + +import io.helidon.webclient.api.ClientUri; + +/** + * Interface implemented by all client URI suppliers. + */ +public interface ClientUriSupplier extends Iterator, Iterable { + + @Override + default Iterator iterator() { + return this; + } +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientUriSuppliers.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientUriSuppliers.java new file mode 100644 index 00000000000..9c279f7a370 --- /dev/null +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/ClientUriSuppliers.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.webclient.grpc; + +import java.net.URI; +import java.security.SecureRandom; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import io.helidon.webclient.api.ClientUri; + +/** + * Some popular implementations of the {@link io.helidon.webclient.grpc.ClientUriSupplier} + * interface. + */ +public class ClientUriSuppliers { + + /** + * Supplies an iterator that returns URIs chosen in order from + * first to last. + */ + public static class OrderedSupplier implements ClientUriSupplier { + + private final Iterator clientUris; + + /** + * Creates an ordered supplier. + * + * @param clientUris array of client URIs + * @return new supplier + */ + public static OrderedSupplier create(ClientUri... clientUris) { + return new OrderedSupplier(List.of(clientUris)); + } + + /** + * Creates an ordered supplier. + * + * @param clientUris collection of client URIs + * @return new supplier + */ + public static OrderedSupplier create(Collection clientUris) { + return new OrderedSupplier(clientUris); + } + + protected OrderedSupplier(Collection clientUris) { + this.clientUris = List.copyOf(clientUris).iterator(); + } + + @Override + public boolean hasNext() { + return clientUris.hasNext(); + } + + @Override + public ClientUri next() { + return clientUris.next(); + } + } + + /** + * Supplies a neven-ending iterator that returns URIs chosen using + * a round-robin strategy. + */ + public static class RoundRobinSupplier implements ClientUriSupplier { + + private int next; + private final ClientUri[] clientUris; + + /** + * Creates a round-robin supplier. + * + * @param clientUris array of client URIs + * @return new supplier + */ + public static RoundRobinSupplier create(ClientUri... clientUris) { + return new RoundRobinSupplier(clientUris); + } + + /** + * Creates a round-robin supplier. + * + * @param clientUris collection of client URIs + * @return new supplier + */ + public static RoundRobinSupplier create(Collection clientUris) { + return new RoundRobinSupplier(clientUris.toArray(new ClientUri[]{})); + } + + protected RoundRobinSupplier(ClientUri[] clientUris) { + this.clientUris = clientUris; + } + + @Override + public boolean hasNext() { + return true; + } + + @Override + public ClientUri next() { + return clientUris[next++ % clientUris.length]; + } + } + + /** + * Supplies the same client URI over and over, never ends. + */ + public static class SingleSupplier implements ClientUriSupplier { + + private final ClientUri clientUri; + + /** + * Creates a single supplier. + * + * @param clientUri the client URI as a string + * @return new supplier + */ + public static SingleSupplier create(String clientUri) { + return new SingleSupplier(ClientUri.create(URI.create(clientUri))); + } + + /** + * Creates a single supplier. + * + * @param clientUri the client URI + * @return new supplier + */ + public static SingleSupplier create(ClientUri clientUri) { + return new SingleSupplier(clientUri); + } + + protected SingleSupplier(ClientUri clientUri) { + this.clientUri = clientUri; + } + + @Override + public boolean hasNext() { + return true; + } + + @Override + public ClientUri next() { + return clientUri; + } + } + + /** + * Supplies an iterator that returns a URI chosen at random, never ends. + */ + public static class RandomSupplier implements ClientUriSupplier { + + private final ClientUri[] clientUris; + private final SecureRandom random = new SecureRandom(); + + /** + * Creates a random supplier. + * + * @param clientUris array of client URIs + * @return new supplier + */ + public static RandomSupplier create(ClientUri... clientUris) { + return new RandomSupplier(clientUris); + } + + /** + * Creates a random supplier. + * + * @param clientUris collection of client URIs + * @return new supplier + */ + public static RandomSupplier create(Collection clientUris) { + return new RandomSupplier(clientUris.toArray(new ClientUri[]{})); + } + + protected RandomSupplier(ClientUri[] clientUris) { + this.clientUris = clientUris; + } + + @Override + public boolean hasNext() { + return true; + } + + @Override + public ClientUri next() { + return clientUris[random.nextInt(clientUris.length)]; + } + } +} diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java index 65ebfced73d..4186ed74597 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcBaseClientCall.java @@ -66,12 +66,14 @@ abstract class GrpcBaseClientCall extends ClientCall { protected static final BufferData EMPTY_BUFFER_DATA = BufferData.empty(); private final GrpcClientImpl grpcClient; + private final GrpcChannel grpcChannel; private final MethodDescriptor methodDescriptor; private final CallOptions callOptions; private final int initBufferSize; private final Duration pollWaitTime; private final boolean abortPollTimeExpired; private final Duration heartbeatPeriod; + private final ClientUriSupplier clientUriSupplier; private final MethodDescriptor.Marshaller requestMarshaller; private final MethodDescriptor.Marshaller responseMarshaller; @@ -81,8 +83,9 @@ abstract class GrpcBaseClientCall extends ClientCall { private volatile Listener responseListener; private volatile HelidonSocket socket; - GrpcBaseClientCall(GrpcClientImpl grpcClient, MethodDescriptor methodDescriptor, CallOptions callOptions) { - this.grpcClient = grpcClient; + GrpcBaseClientCall(GrpcChannel grpcChannel, MethodDescriptor methodDescriptor, CallOptions callOptions) { + this.grpcClient = (GrpcClientImpl) grpcChannel.grpcClient(); + this.grpcChannel = grpcChannel; this.methodDescriptor = methodDescriptor; this.callOptions = callOptions; this.requestMarshaller = methodDescriptor.getRequestMarshaller(); @@ -91,6 +94,7 @@ abstract class GrpcBaseClientCall extends ClientCall { this.pollWaitTime = grpcClient.prototype().protocolConfig().pollWaitTime(); this.abortPollTimeExpired = grpcClient.prototype().protocolConfig().abortPollTimeExpired(); this.heartbeatPeriod = grpcClient.prototype().protocolConfig().heartbeatPeriod(); + this.clientUriSupplier = grpcClient.prototype().clientUriSupplier().orElse(null); } @Override @@ -100,10 +104,11 @@ public void start(Listener responseListener, Metadata metadata) { this.responseListener = responseListener; // obtain HTTP2 connection - ClientConnection clientConnection = clientConnection(); + ClientUri clientUri = nextClientUri(); + ClientConnection clientConnection = clientConnection(clientUri); socket = clientConnection.helidonSocket(); connection = Http2ClientConnection.create((Http2ClientImpl) grpcClient.http2Client(), - clientConnection, true); + clientConnection, true); // create HTTP2 stream from connection clientStream = new GrpcClientStream( @@ -134,7 +139,6 @@ public Duration readTimeout() { startStreamingThreads(); // send HEADERS frame - ClientUri clientUri = grpcClient.prototype().baseUri().orElseThrow(); WritableHeaders headers = WritableHeaders.create(); headers.add(Http2Headers.AUTHORITY_NAME, clientUri.authority()); headers.add(Http2Headers.METHOD_NAME, "POST"); @@ -165,11 +169,13 @@ protected void unblockUnaryExecutor() { } } - protected ClientConnection clientConnection() { - GrpcClientConfig clientConfig = grpcClient.prototype(); - ClientUri clientUri = clientConfig.baseUri().orElseThrow(); - WebClient webClient = grpcClient.webClient(); + protected GrpcClientImpl grpcClient() { + return grpcClient; + } + protected ClientConnection clientConnection(ClientUri clientUri) { + WebClient webClient = grpcClient.webClient(); + GrpcClientConfig clientConfig = grpcClient.prototype(); ConnectionKey connectionKey = new ConnectionKey( clientUri.scheme(), clientUri.host(), @@ -179,13 +185,12 @@ protected ClientConnection clientConnection() { DefaultDnsResolver.create(), DnsAddressLookup.defaultLookup(), Proxy.noProxy()); - return TcpClientConnection.create(webClient, - connectionKey, - Collections.emptyList(), - connection -> false, - connection -> { - }).connect(); + connectionKey, + Collections.emptyList(), + connection -> false, + connection -> { + }).connect(); } protected boolean isRemoteOpen() { @@ -245,4 +250,16 @@ protected Listener responseListener() { protected HelidonSocket socket() { return socket; } + + /** + * Retrieves the next URI either from the supplier or directly from config. If + * a supplier is provided, it will take precedence. + * + * @return the next {@link ClientUri} + * @throws java.util.NoSuchElementException if supplier has been exhausted + */ + private ClientUri nextClientUri() { + return clientUriSupplier == null ? grpcClient.prototype().baseUri().orElseThrow() + : clientUriSupplier.next(); + } } diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java index f4d5e006cb7..caa4df37b78 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcChannel.java @@ -53,8 +53,8 @@ public ClientCall newCall( MethodDescriptor methodDescriptor, CallOptions callOptions) { MethodDescriptor.MethodType methodType = methodDescriptor.getType(); return methodType == MethodDescriptor.MethodType.UNARY - ? new GrpcUnaryClientCall<>(grpcClient, methodDescriptor, callOptions) - : new GrpcClientCall<>(grpcClient, methodDescriptor, callOptions); + ? new GrpcUnaryClientCall<>(this, methodDescriptor, callOptions) + : new GrpcClientCall<>(this, methodDescriptor, callOptions); } @Override diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java index 96573554350..8016ce42438 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientCall.java @@ -60,9 +60,9 @@ class GrpcClientCall extends GrpcBaseClientCall { private volatile Future writeStreamFuture; private volatile Future heartbeatFuture; - GrpcClientCall(GrpcClientImpl grpcClient, MethodDescriptor methodDescriptor, CallOptions callOptions) { - super(grpcClient, methodDescriptor, callOptions); - this.executor = grpcClient.webClient().executor(); + GrpcClientCall(GrpcChannel grpcChannel, MethodDescriptor methodDescriptor, CallOptions callOptions) { + super(grpcChannel, methodDescriptor, callOptions); + this.executor = grpcClient().webClient().executor(); } @Override diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java index 7403c76a0ad..f6dd3f678e9 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcClientConfigBlueprint.java @@ -16,6 +16,8 @@ package io.helidon.webclient.grpc; +import java.util.Optional; + import io.helidon.builder.api.Option; import io.helidon.builder.api.Prototype; import io.helidon.webclient.api.HttpClientConfig; @@ -35,4 +37,13 @@ interface GrpcClientConfigBlueprint extends HttpClientConfig, Prototype.Factory< @Option.Default("create()") @Option.Configured GrpcClientProtocolConfig protocolConfig(); + + /** + * A {@link io.helidon.webclient.grpc.ClientUriSupplier} that can dynamically + * provide zero or more {@link io.helidon.webclient.api.ClientUri}s to connect. + * + * @return a supplier for zero or more client URIs + */ + Optional clientUriSupplier(); } + diff --git a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java index 2ce34db5d5b..355932991a6 100644 --- a/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java +++ b/webclient/grpc/src/main/java/io/helidon/webclient/grpc/GrpcUnaryClientCall.java @@ -42,10 +42,10 @@ class GrpcUnaryClientCall extends GrpcBaseClientCall { private volatile boolean requestSent; private volatile boolean responseSent; - GrpcUnaryClientCall(GrpcClientImpl grpcClient, + GrpcUnaryClientCall(GrpcChannel grpcChannel, MethodDescriptor methodDescriptor, CallOptions callOptions) { - super(grpcClient, methodDescriptor, callOptions); + super(grpcChannel, methodDescriptor, callOptions); } @Override diff --git a/webclient/http1/pom.xml b/webclient/http1/pom.xml index e21033c0984..4460015f3df 100644 --- a/webclient/http1/pom.xml +++ b/webclient/http1/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-http1 diff --git a/webclient/http2/pom.xml b/webclient/http2/pom.xml index bf32f271791..81f96a76d0b 100644 --- a/webclient/http2/pom.xml +++ b/webclient/http2/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-http2 diff --git a/webclient/metrics/pom.xml b/webclient/metrics/pom.xml index 114777762fa..9ca16cdbf69 100644 --- a/webclient/metrics/pom.xml +++ b/webclient/metrics/pom.xml @@ -22,7 +22,7 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-metrics diff --git a/webclient/pom.xml b/webclient/pom.xml index e10e2b40719..c3c140cd471 100644 --- a/webclient/pom.xml +++ b/webclient/pom.xml @@ -22,7 +22,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.webclient helidon-webclient-project diff --git a/webclient/security/pom.xml b/webclient/security/pom.xml index f899a4f1c18..f6ae575edce 100644 --- a/webclient/security/pom.xml +++ b/webclient/security/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-security diff --git a/webclient/sse/pom.xml b/webclient/sse/pom.xml index 89f24ae20bb..93ae1a8ac6e 100644 --- a/webclient/sse/pom.xml +++ b/webclient/sse/pom.xml @@ -23,7 +23,7 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-sse diff --git a/webclient/sse/src/main/java/io/helidon/webclient/sse/SseSourceHandlerProvider.java b/webclient/sse/src/main/java/io/helidon/webclient/sse/SseSourceHandlerProvider.java index 502bb1f8989..e8758e16977 100644 --- a/webclient/sse/src/main/java/io/helidon/webclient/sse/SseSourceHandlerProvider.java +++ b/webclient/sse/src/main/java/io/helidon/webclient/sse/SseSourceHandlerProvider.java @@ -25,6 +25,7 @@ import java.time.Duration; import io.helidon.common.GenericType; +import io.helidon.common.buffers.DataReader; import io.helidon.common.media.type.MediaTypes; import io.helidon.http.media.MediaContext; import io.helidon.http.sse.SseEvent; @@ -93,6 +94,9 @@ public > void handle(X source, HttpClientResponse res } } + source.onClose(); + } catch (DataReader.InsufficientDataAvailableException e) { + // normal SSE termination when connection closed by server source.onClose(); } catch (IOException e) { source.onError(e); diff --git a/webclient/tests/grpc/pom.xml b/webclient/tests/grpc/pom.xml index 2934bb336bc..38a51ad88e2 100644 --- a/webclient/tests/grpc/pom.xml +++ b/webclient/tests/grpc/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient.tests helidon-webclient-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-tests-grpc @@ -104,6 +104,11 @@ helidon-webserver-testing-junit5-grpc test + + io.helidon.fault-tolerance + helidon-fault-tolerance + test + diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/ClientUriSuppliersTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/ClientUriSuppliersTest.java new file mode 100644 index 00000000000..e2fe0f5cf71 --- /dev/null +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/ClientUriSuppliersTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.webclient.grpc.tests; + +import java.net.URI; +import java.util.stream.IntStream; + +import io.helidon.webclient.api.ClientUri; + +import org.junit.jupiter.api.Test; + +import static io.helidon.webclient.grpc.ClientUriSuppliers.OrderedSupplier; +import static io.helidon.webclient.grpc.ClientUriSuppliers.RandomSupplier; +import static io.helidon.webclient.grpc.ClientUriSuppliers.RoundRobinSupplier; +import static io.helidon.webclient.grpc.ClientUriSuppliers.SingleSupplier; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.collection.IsIn.isIn; +import static org.hamcrest.MatcherAssert.assertThat; + +class ClientUriSuppliersTest { + + private static final ClientUri[] CLIENT_URIS = { + ClientUri.create(URI.create("http://localhost:8000")), + ClientUri.create(URI.create("http://localhost:8001")), + ClientUri.create(URI.create("http://localhost:8002")) + }; + + @Test + void testOrderedSupplier() { + OrderedSupplier supplier = OrderedSupplier.create(CLIENT_URIS); + assertThat(supplier.hasNext(), is(true)); + assertThat(supplier.next(), is(CLIENT_URIS[0])); + assertThat(supplier.hasNext(), is(true)); + assertThat(supplier.next(), is(CLIENT_URIS[1])); + assertThat(supplier.hasNext(), is(true)); + assertThat(supplier.next(), is(CLIENT_URIS[2])); + assertThat(supplier.hasNext(), is(false)); + } + + @Test + void testRoundRobinSupplier() { + RoundRobinSupplier supplier = RoundRobinSupplier.create(CLIENT_URIS); + IntStream.range(0, 5).forEach(i -> { + assertThat(supplier.hasNext(), is(true)); + assertThat(supplier.next(), is(CLIENT_URIS[0])); + assertThat(supplier.hasNext(), is(true)); + assertThat(supplier.next(), is(CLIENT_URIS[1])); + assertThat(supplier.hasNext(), is(true)); + assertThat(supplier.next(), is(CLIENT_URIS[2])); + }); + } + + @Test + void testSingleSupplier() { + SingleSupplier supplier = SingleSupplier.create(CLIENT_URIS[0]); + IntStream.range(0, 5).forEach(i -> { + assertThat(supplier.hasNext(), is(true)); + assertThat(supplier.next(), is(CLIENT_URIS[0])); + }); + } + + @Test + void testRandomSupplier() { + RandomSupplier supplier = RandomSupplier.create(CLIENT_URIS); + IntStream.range(0, 5).forEach(i -> { + assertThat(supplier.hasNext(), is(true)); + assertThat(supplier.next(), isIn(CLIENT_URIS)); + }); + } +} diff --git a/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcClientUriTest.java b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcClientUriTest.java new file mode 100644 index 00000000000..a602688d6b4 --- /dev/null +++ b/webclient/tests/grpc/src/test/java/io/helidon/webclient/grpc/tests/GrpcClientUriTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webclient.grpc.tests; + +import java.net.URI; +import java.util.concurrent.CountDownLatch; + +import io.helidon.common.configurable.Resource; +import io.helidon.common.tls.Tls; +import io.helidon.faulttolerance.Retry; +import io.helidon.webclient.api.ClientUri; +import io.helidon.webclient.grpc.ClientUriSuppliers; +import io.helidon.webclient.grpc.GrpcClient; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.testing.junit5.ServerTest; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests client URI suppliers. + */ +@ServerTest +class GrpcClientUriTest extends GrpcBaseTest { + + private final WebServer server; + private final Tls clientTls; + + GrpcClientUriTest(WebServer server) { + this.server = server; + this.clientTls = Tls.builder() + .trust(trust -> trust + .keystore(store -> store + .passphrase("password") + .trustStore(true) + .keystore(Resource.create("client.p12")))) + .build(); + } + + /** + * Shows how each gRPC call advances the iterator in the {@code ClientUriSupplier}. + */ + @Test + void testSupplier() { + CountDownLatch latch = new CountDownLatch(2); + ClientUri clientUri = ClientUri.create(URI.create("https://localhost:" + server.port())); + GrpcClient grpcClient = GrpcClient.builder() + .tls(clientTls) + .clientUriSupplier(new ClientUriSupplierTest(latch, clientUri, clientUri)) + .build(); + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient.channel()); + + Strings.StringMessage res1 = service.upper(newStringMessage("hello")); + assertThat(res1.getText(), is("HELLO")); + Strings.StringMessage res2 = service.upper(newStringMessage("hello")); + assertThat(res2.getText(), is("HELLO")); + assertThat(latch.getCount(), is(0L)); + } + + /** + * Should fail to connect to first URI but succeed with second after retrying. + */ + @Test + void testSupplierWithRetries() { + CountDownLatch latch = new CountDownLatch(2); + ClientUri badUri = ClientUri.create(URI.create("https://foo:8000")); + ClientUri goodUri = ClientUri.create(URI.create("https://localhost:" + server.port())); + GrpcClient grpcClient = GrpcClient.builder() + .tls(clientTls) + .clientUriSupplier(new ClientUriSupplierTest(latch, badUri, goodUri)) + .build(); + StringServiceGrpc.StringServiceBlockingStub service = StringServiceGrpc.newBlockingStub(grpcClient.channel()); + + Retry retry = Retry.builder().calls(2).build(); + Strings.StringMessage res = retry.invoke(() -> service.upper(newStringMessage("hello"))); + assertThat(res.getText(), is("HELLO")); + assertThat(latch.getCount(), is(0L)); + } + + static class ClientUriSupplierTest extends ClientUriSuppliers.RoundRobinSupplier { + + private final CountDownLatch latch; + + ClientUriSupplierTest(CountDownLatch latch, ClientUri... clientUris) { + super(clientUris); + this.latch = latch; + } + + @Override + public ClientUri next() { + latch.countDown(); // should be called twice + return super.next(); + } + } +} diff --git a/webclient/tests/http1/pom.xml b/webclient/tests/http1/pom.xml index cd2d23777f0..ee7a3519903 100644 --- a/webclient/tests/http1/pom.xml +++ b/webclient/tests/http1/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient.tests helidon-webclient-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-tests-http1 diff --git a/webclient/tests/http2/pom.xml b/webclient/tests/http2/pom.xml index aca82463d1c..79c4ab89ca6 100644 --- a/webclient/tests/http2/pom.xml +++ b/webclient/tests/http2/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient.tests helidon-webclient-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-tests-http2 diff --git a/webclient/tests/pom.xml b/webclient/tests/pom.xml index 31fa199711e..94a4ea7a68b 100644 --- a/webclient/tests/pom.xml +++ b/webclient/tests/pom.xml @@ -24,7 +24,7 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.webclient.tests diff --git a/webclient/tests/webclient/pom.xml b/webclient/tests/webclient/pom.xml index 855dc1e41b4..7af7e955858 100644 --- a/webclient/tests/webclient/pom.xml +++ b/webclient/tests/webclient/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient.tests helidon-webclient-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-tests-webclient @@ -124,5 +124,10 @@ helidon-logging-jul test + + org.mockito + mockito-core + test + diff --git a/webclient/tests/webclient/src/test/java/io/helidon/webclient/tests/WebClientMockTest.java b/webclient/tests/webclient/src/test/java/io/helidon/webclient/tests/WebClientMockTest.java new file mode 100644 index 00000000000..a560ff22b55 --- /dev/null +++ b/webclient/tests/webclient/src/test/java/io/helidon/webclient/tests/WebClientMockTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.webclient.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.helidon.http.HeaderName; +import io.helidon.http.HeaderNames; +import io.helidon.http.Status; +import io.helidon.webclient.api.HttpClientRequest; +import io.helidon.webclient.api.HttpClientResponse; +import io.helidon.webclient.api.WebClient; + +import org.junit.jupiter.api.Test; + +/** + * Tests WebClientMockTest. + */ +class WebClientMockTest { + + @Test + public void useMock() { + WebClient webClient = mockWebClient(Status.OK_200); + HttpClientResponse response = webClient.post("http://test.com") + .header(HeaderNames.AUTHORIZATION, "Bearer asdsadsad") + .submit(new Object()); + assertEquals(Status.OK_200, response.status()); + } + + private WebClient mockWebClient(Status status) { + WebClient webClient = mock(WebClient.class); + HttpClientRequest request = mock(HttpClientRequest.class); + HttpClientResponse response = mock(HttpClientResponse.class); + doReturn(request).when(webClient).post(anyString()); + doReturn(request).when(request).header(any(HeaderName.class), anyString()); + doReturn(response).when(request).submit(any(Object.class)); + when(response.status()).thenReturn(status); + return webClient; + } + +} diff --git a/webclient/tracing/pom.xml b/webclient/tracing/pom.xml index 98712d72b4d..8465e55e252 100644 --- a/webclient/tracing/pom.xml +++ b/webclient/tracing/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-tracing diff --git a/webclient/webclient/pom.xml b/webclient/webclient/pom.xml index 8c7bdc65f89..45c8e5cd5c8 100644 --- a/webclient/webclient/pom.xml +++ b/webclient/webclient/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient diff --git a/webclient/websocket/pom.xml b/webclient/websocket/pom.xml index 766e5fe7b93..b791bb7b310 100644 --- a/webclient/websocket/pom.xml +++ b/webclient/websocket/pom.xml @@ -21,7 +21,7 @@ io.helidon.webclient helidon-webclient-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webclient-websocket diff --git a/webserver/access-log/pom.xml b/webserver/access-log/pom.xml index 4e083d987ce..f33b809c1d2 100644 --- a/webserver/access-log/pom.xml +++ b/webserver/access-log/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-access-log diff --git a/webserver/concurrency-limits/pom.xml b/webserver/concurrency-limits/pom.xml new file mode 100644 index 00000000000..61bb4bb3010 --- /dev/null +++ b/webserver/concurrency-limits/pom.xml @@ -0,0 +1,129 @@ + + + + 4.0.0 + + io.helidon.webserver + helidon-webserver-project + 4.2.0-SNAPSHOT + + + helidon-webserver-concurrency-limits + Helidon WebServer Concurrency Limits + Feature that adds filters for concurrency limits + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.common.concurrency + helidon-common-concurrency-limits + + + io.helidon.common + helidon-common-config + + + helidon-builder-api + io.helidon.builder + + + io.helidon.webserver.testing.junit5 + helidon-webserver-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.logging + helidon-logging-jul + test + + + io.helidon.config + helidon-config-yaml + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.config.metadata + helidon-config-metadata-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.builder + helidon-builder-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + diff --git a/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsFeature.java b/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsFeature.java new file mode 100644 index 00000000000..62f77a2cb10 --- /dev/null +++ b/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsFeature.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.concurrency.limits; + +import java.util.Set; +import java.util.function.Consumer; + +import io.helidon.builder.api.RuntimeType; +import io.helidon.common.Weighted; +import io.helidon.common.config.Config; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.spi.ServerFeature; + +/** + * Server feature that adds limits as filters. + *

+ * When using this feature, the limits operation is enforced within a filter, i.e. after the request + * is accepted. This means it is used only for HTTP requests. + */ +@RuntimeType.PrototypedBy(LimitsFeatureConfig.class) +public class LimitsFeature implements ServerFeature, Weighted, RuntimeType.Api { + /** + * Default weight of this feature. It is the first feature to be registered (above context and access log). + *

+ * Context: 1100 + *

+ * Access Log: 1000 + *

+ * This feature: {@value} + */ + public static final double WEIGHT = 2000; + static final String ID = "limits"; + + private final LimitsFeatureConfig config; + + private LimitsFeature(LimitsFeatureConfig config) { + this.config = config; + } + + /** + * Fluent API builder to set up an instance. + * + * @return a new builder + */ + public static LimitsFeatureConfig.Builder builder() { + return LimitsFeatureConfig.builder(); + } + + /** + * Create a new instance from its configuration. + * + * @param config configuration + * @return a new feature + */ + public static LimitsFeature create(LimitsFeatureConfig config) { + return new LimitsFeature(config); + } + + /** + * Create a new instance customizing its configuration. + * + * @param builderConsumer consumer of configuration + * @return a new feature + */ + public static LimitsFeature create(Consumer builderConsumer) { + return builder() + .update(builderConsumer) + .build(); + } + + /** + * Create a new limits feature with default setup, but enabled. + * + * @return a new feature + */ + public static LimitsFeature create() { + return builder() + .enabled(true) + .build(); + } + + /** + * Create a new context feature with custom setup. + * + * @param config configuration + * @return a new configured feature + */ + public static LimitsFeature create(Config config) { + return builder() + .config(config) + .build(); + } + + @Override + public void setup(ServerFeatureContext featureContext) { + double featureWeight = config.weight(); + // all sockets + Set sockets = config.sockets(); + if (sockets.isEmpty()) { + // configure on default only + featureContext.socket(WebServer.DEFAULT_SOCKET_NAME) + .httpRouting() + .addFeature(new LimitsRoutingFeature(config, featureWeight)); + } else { + // configure on all configured + for (String socket : sockets) { + featureContext.socket(socket) + .httpRouting() + .addFeature(new LimitsRoutingFeature(config, featureWeight)); + } + } + } + + @Override + public String name() { + return config.name(); + } + + @Override + public String type() { + return ID; + } + + @Override + public double weight() { + return config.weight(); + } + + @Override + public LimitsFeatureConfig prototype() { + return config; + } +} diff --git a/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsFeatureConfigBlueprint.java b/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsFeatureConfigBlueprint.java new file mode 100644 index 00000000000..dc681c4c172 --- /dev/null +++ b/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsFeatureConfigBlueprint.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.concurrency.limits; + +import java.util.Optional; +import java.util.Set; + +import io.helidon.builder.api.Option; +import io.helidon.builder.api.Prototype; +import io.helidon.common.concurrency.limits.Limit; +import io.helidon.common.concurrency.limits.spi.LimitProvider; +import io.helidon.webserver.spi.ServerFeatureProvider; + +@Prototype.Blueprint +@Prototype.Configured(value = LimitsFeature.ID, root = false) +@Prototype.Provides(ServerFeatureProvider.class) +interface LimitsFeatureConfigBlueprint extends Prototype.Factory { + /** + * Weight of the context feature. As it is used by other features, the default is quite high: + * {@value LimitsFeature#WEIGHT}. + * + * @return weight of the feature + */ + @Option.DefaultDouble(LimitsFeature.WEIGHT) + @Option.Configured + double weight(); + + /** + * List of sockets to register this feature on. If empty, it would get registered on all sockets. + * + * @return socket names to register on, defaults to empty (all available sockets) + */ + @Option.Configured + Set sockets(); + + /** + * Name of this instance. + * + * @return instance name + */ + @Option.Default(LimitsFeature.ID) + String name(); + + /** + * Concurrency limit to use to limit concurrent execution of incoming requests. + * The default is to have unlimited concurrency. + * + * @return concurrency limit + */ + @Option.Provider(value = LimitProvider.class, discoverServices = false) + @Option.Configured + Optional concurrencyLimit(); + + /** + * Whether this feature is enabled, defaults to {@code true}. + * + * @return whether to enable this feature + */ + @Option.DefaultBoolean(true) + @Option.Configured + boolean enabled(); +} diff --git a/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsFeatureProvider.java b/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsFeatureProvider.java new file mode 100644 index 00000000000..5d26cf8e461 --- /dev/null +++ b/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsFeatureProvider.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.concurrency.limits; + +import io.helidon.common.Weight; +import io.helidon.common.config.Config; +import io.helidon.webserver.spi.ServerFeatureProvider; + +/** + * {@link java.util.ServiceLoader} provider implementation to automatically register this service. + *

+ * The required configuration (disabled by default): + *

+ * server:
+ *   features:
+ *     limits:
+ *       enabled: true
+ *       limit:
+ *         bulkhead:
+ *         limit: 10
+ *         queue: 100
+ * 
+ */ +@Weight(LimitsFeature.WEIGHT) +public class LimitsFeatureProvider implements ServerFeatureProvider { + /** + * Public constructor required by {@link java.util.ServiceLoader}. + */ + public LimitsFeatureProvider() { + } + + @Override + public String configKey() { + return LimitsFeature.ID; + } + + @Override + public LimitsFeature create(Config config, String name) { + return LimitsFeature.builder() + .config(config) + .name(name) + .build(); + } +} diff --git a/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsRoutingFeature.java b/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsRoutingFeature.java new file mode 100644 index 00000000000..4e8fc48328c --- /dev/null +++ b/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/LimitsRoutingFeature.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.concurrency.limits; + +import java.util.Optional; + +import io.helidon.common.Weighted; +import io.helidon.common.concurrency.limits.Limit; +import io.helidon.common.concurrency.limits.LimitAlgorithm; +import io.helidon.http.HttpException; +import io.helidon.http.Status; +import io.helidon.webserver.http.FilterChain; +import io.helidon.webserver.http.HttpFeature; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.http.RoutingRequest; +import io.helidon.webserver.http.RoutingResponse; + +class LimitsRoutingFeature implements HttpFeature, Weighted { + private final double featureWeight; + private final Limit limits; + private final boolean enabled; + + LimitsRoutingFeature(LimitsFeatureConfig config, double featureWeight) { + this.featureWeight = featureWeight; + this.limits = config.concurrencyLimit().orElse(null); + this.enabled = config.enabled(); + } + + @Override + public void setup(HttpRouting.Builder builder) { + if (enabled && limits != null) { + builder.addFilter(this::filter); + } + } + + @Override + public double weight() { + return featureWeight; + } + + private void filter(FilterChain chain, RoutingRequest req, RoutingResponse res) { + Optional token = limits.tryAcquire(); + + if (token.isEmpty()) { + throw new HttpException("Limit exceeded", Status.SERVICE_UNAVAILABLE_503); + } + + LimitAlgorithm.Token permit = token.get(); + try { + chain.proceed(); + permit.success(); + } catch (Throwable e) { + permit.dropped(); + throw e; + } + } +} diff --git a/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/package-info.java b/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/package-info.java new file mode 100644 index 00000000000..d1075d62ffb --- /dev/null +++ b/webserver/concurrency-limits/src/main/java/io/helidon/webserver/concurrency/limits/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * An implementation of a feature to protect all server requests with a limit. + */ +package io.helidon.webserver.concurrency.limits; diff --git a/webserver/concurrency-limits/src/main/java/module-info.java b/webserver/concurrency-limits/src/main/java/module-info.java new file mode 100644 index 00000000000..d1d99c99b8a --- /dev/null +++ b/webserver/concurrency-limits/src/main/java/module-info.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Limits feature for Helidon WebServer. + */ +module io.helidon.webserver.concurrency.limits { + requires io.helidon.common; + requires io.helidon.http; + requires io.helidon.webserver; + + requires transitive io.helidon.builder.api; + requires transitive io.helidon.common.config; + requires transitive io.helidon.common.concurrency.limits; + + exports io.helidon.webserver.concurrency.limits; + + provides io.helidon.webserver.spi.ServerFeatureProvider + with io.helidon.webserver.concurrency.limits.LimitsFeatureProvider; + + uses io.helidon.common.concurrency.limits.spi.LimitProvider; +} \ No newline at end of file diff --git a/webserver/concurrency-limits/src/test/java/io/helidon/webserver/concurrency/limits/FixedLimitTest.java b/webserver/concurrency-limits/src/test/java/io/helidon/webserver/concurrency/limits/FixedLimitTest.java new file mode 100644 index 00000000000..71921d7ba81 --- /dev/null +++ b/webserver/concurrency-limits/src/test/java/io/helidon/webserver/concurrency/limits/FixedLimitTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.concurrency.limits; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import io.helidon.http.Status; +import io.helidon.webclient.api.ClientResponseTyped; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +public class FixedLimitTest { + private static final CountDownLatch FIRST_ENCOUNTER = new CountDownLatch(1); + private static final CountDownLatch FINISH_LATCH = new CountDownLatch(1); + + private final Http1Client client; + + public FixedLimitTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + public static void route(HttpRules rules) { + rules.get("/greet", (req, res) -> res.send("Hello")) + .get("/wait", (req, res) -> { + FIRST_ENCOUNTER.countDown(); + FINISH_LATCH.await(); + res.send("finished"); + }); + } + + @Test + public void testRequest() { + var response = client.get("/greet") + .request(String.class); + + assertThat(response.status(), is(Status.OK_200)); + assertThat(response.entity(), is("Hello")); + } + + @Test + public void testLimits() throws Exception { + Callable> callable = () -> { + return client.get("/wait") + .request(String.class); + }; + try (ExecutorService es = Executors.newThreadPerTaskExecutor(Thread.ofVirtual().factory())) { + var first = es.submit(callable); + FIRST_ENCOUNTER.await(); + var secondResponse = es.submit(callable) + .get(5, TimeUnit.SECONDS); + + assertThat(secondResponse.status(), is(Status.SERVICE_UNAVAILABLE_503)); + FINISH_LATCH.countDown(); + var firstResponse = first.get(5, TimeUnit.SECONDS); + assertThat(firstResponse.status(), is(Status.OK_200)); + assertThat(firstResponse.entity(), is("finished")); + + } + } +} diff --git a/webserver/concurrency-limits/src/test/resources/application.yaml b/webserver/concurrency-limits/src/test/resources/application.yaml new file mode 100644 index 00000000000..beb5d7af950 --- /dev/null +++ b/webserver/concurrency-limits/src/test/resources/application.yaml @@ -0,0 +1,24 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + features: + limits: + concurrency-limit: + fixed: + permits: 1 + queue-length: 0 + diff --git a/webserver/concurrency-limits/src/test/resources/logging.properties b/webserver/concurrency-limits/src/test/resources/logging.properties new file mode 100644 index 00000000000..eb45bdbe3fa --- /dev/null +++ b/webserver/concurrency-limits/src/test/resources/logging.properties @@ -0,0 +1,21 @@ +# +# Copyright (c) 2024 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.webserver.level=INFO diff --git a/webserver/context/pom.xml b/webserver/context/pom.xml index 3c972843785..2567a86ea2e 100644 --- a/webserver/context/pom.xml +++ b/webserver/context/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-context diff --git a/webserver/cors/pom.xml b/webserver/cors/pom.xml index 39d53d73b65..e4bf0b1e783 100644 --- a/webserver/cors/pom.xml +++ b/webserver/cors/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-cors diff --git a/webserver/graphql/pom.xml b/webserver/graphql/pom.xml index cb38805b571..b90bd1df8c0 100644 --- a/webserver/graphql/pom.xml +++ b/webserver/graphql/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/webserver/grpc/pom.xml b/webserver/grpc/pom.xml index 4094cd8a89d..032d6d442a3 100644 --- a/webserver/grpc/pom.xml +++ b/webserver/grpc/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-grpc diff --git a/webserver/http2/pom.xml b/webserver/http2/pom.xml index 9b593187d6e..85461395674 100644 --- a/webserver/http2/pom.xml +++ b/webserver/http2/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-http2 @@ -44,6 +44,10 @@ io.helidon.builder helidon-builder-api + + io.helidon.common.concurrency + helidon-common-concurrency-limits + io.helidon.common.features helidon-common-features-api diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java index fa70fa15fb9..295672a6130 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java @@ -27,6 +27,8 @@ import io.helidon.common.buffers.BufferData; import io.helidon.common.buffers.DataReader; +import io.helidon.common.concurrency.limits.FixedLimit; +import io.helidon.common.concurrency.limits.Limit; import io.helidon.common.task.InterruptableTask; import io.helidon.common.tls.TlsUtils; import io.helidon.http.DateTime; @@ -172,9 +174,9 @@ private static void applySetting(Http2Settings.Builder builder, long value, Http } @Override - public void handle(Semaphore requestSemaphore) throws InterruptedException { + public void handle(Limit limit) throws InterruptedException { try { - doHandle(requestSemaphore); + doHandle(limit); } catch (Http2Exception e) { if (state == State.FINISHED) { // already handled @@ -209,6 +211,12 @@ public void handle(Semaphore requestSemaphore) throws InterruptedException { } } + @SuppressWarnings("removal") + @Override + public void handle(Semaphore requestSemaphore) throws InterruptedException { + handle(FixedLimit.create(requestSemaphore)); + } + /** * Client settings, obtained from SETTINGS frame or HTTP/2 upgrade request. * @@ -326,7 +334,7 @@ Http2Settings clientSettings() { return clientSettings; } - private void doHandle(Semaphore requestSemaphore) throws InterruptedException { + private void doHandle(Limit limit) throws InterruptedException { myThread = Thread.currentThread(); while (canRun && state != State.FINISHED) { if (expectPreface && state != State.WRITE_SERVER_SETTINGS) { @@ -341,10 +349,9 @@ private void doHandle(Semaphore requestSemaphore) throws InterruptedException { // no data to read -> connection is closed throw new CloseConnectionException("Connection closed by client", e); } - dispatchHandler(requestSemaphore); - } else { - dispatchHandler(requestSemaphore); } + + dispatchHandler(limit); } if (state != State.FINISHED) { Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.NO_ERROR, "Idle timeout"); @@ -352,7 +359,7 @@ private void doHandle(Semaphore requestSemaphore) throws InterruptedException { } } - private void dispatchHandler(Semaphore requestSemaphore) { + private void dispatchHandler(Limit limit) { switch (state) { case CONTINUATION -> doContinuation(); case WRITE_SERVER_SETTINGS -> writeServerSettings(); @@ -360,7 +367,7 @@ private void dispatchHandler(Semaphore requestSemaphore) { case SETTINGS -> doSettings(); case ACK_SETTINGS -> ackSettings(); case DATA -> dataFrame(); - case HEADERS -> doHeaders(requestSemaphore); + case HEADERS -> doHeaders(limit); case PRIORITY -> doPriority(); case READ_PUSH_PROMISE -> throw new Http2Exception(Http2ErrorCode.REFUSED_STREAM, "Push promise not supported"); case PING -> pingFrame(); @@ -606,7 +613,7 @@ private void dataFrame() { state = State.READ_FRAME; } - private void doHeaders(Semaphore requestSemaphore) { + private void doHeaders(Limit limit) { int streamId = frameHeader.streamId(); StreamContext streamContext = stream(streamId); @@ -675,7 +682,7 @@ private void doHeaders(Semaphore requestSemaphore) { path, http2Config.validatePath()); stream.prologue(httpPrologue); - stream.requestSemaphore(requestSemaphore); + stream.requestLimit(limit); stream.headers(headers, endOfStream); state = State.READ_FRAME; diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java index bee1f707da6..50333162e29 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerStream.java @@ -18,11 +18,15 @@ import java.io.UncheckedIOException; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Semaphore; import io.helidon.common.buffers.BufferData; +import io.helidon.common.concurrency.limits.FixedLimit; +import io.helidon.common.concurrency.limits.Limit; +import io.helidon.common.concurrency.limits.LimitAlgorithm; import io.helidon.common.socket.SocketWriterException; import io.helidon.http.DirectHandler; import io.helidon.http.Header; @@ -99,10 +103,9 @@ class Http2ServerStream implements Runnable, Http2Stream { private Http2SubProtocolSelector.SubProtocolHandler subProtocolHandler; private long expectedLength = -1; private HttpPrologue prologue; - // create a semaphore if accessed before we get the one from connection + // create a limit if accessed before we get the one from connection // must be volatile, as it is accessed both from connection thread and from stream thread - private volatile Semaphore requestSemaphore = new Semaphore(1); - private boolean semaphoreAcquired; + private volatile Limit requestLimit = FixedLimit.create(new Semaphore(1)); /** * A new HTTP/2 server stream. @@ -324,9 +327,6 @@ public void run() { } finally { headers = null; subProtocolHandler = null; - if (semaphoreAcquired) { - requestSemaphore.release(); - } } } @@ -425,8 +425,8 @@ void write100Continue() { } } - void requestSemaphore(Semaphore requestSemaphore) { - this.requestSemaphore = requestSemaphore; + void requestLimit(Limit limit) { + this.requestLimit = limit; } void prologue(HttpPrologue prologue) { @@ -516,15 +516,35 @@ private void handle() { streamId, this::readEntityFromPipeline); Http2ServerResponse response = new Http2ServerResponse(this, request); - semaphoreAcquired = requestSemaphore.tryAcquire(); + try { - if (semaphoreAcquired) { - routing.route(ctx, request, response); - } else { + Optional token = requestLimit.tryAcquire(); + + if (token.isEmpty()) { ctx.log(LOGGER, TRACE, "Too many concurrent requests, rejecting request."); response.status(Status.SERVICE_UNAVAILABLE_503) .send("Too Many Concurrent Requests"); response.commit(); + } else { + LimitAlgorithm.Token permit = token.get(); + try { + routing.route(ctx, request, response); + } finally { + if (response.status() == Status.NOT_FOUND_404) { + permit.ignore(); + } else { + switch (response.status().family()) { + case INFORMATIONAL: + case SUCCESSFUL: + case REDIRECTION: + permit.success(); + break; + default: + permit.dropped(); + break; + } + } + } } } finally { request.content().consume(); diff --git a/webserver/http2/src/main/java/module-info.java b/webserver/http2/src/main/java/module-info.java index 73913df2958..108309d0231 100644 --- a/webserver/http2/src/main/java/module-info.java +++ b/webserver/http2/src/main/java/module-info.java @@ -40,6 +40,7 @@ requires transitive io.helidon.http.media; requires transitive io.helidon.http; requires transitive io.helidon.webserver; + requires transitive io.helidon.common.concurrency.limits; exports io.helidon.webserver.http2; exports io.helidon.webserver.http2.spi; diff --git a/webserver/observe/config/pom.xml b/webserver/observe/config/pom.xml index cca08a03b9f..b8248be047d 100644 --- a/webserver/observe/config/pom.xml +++ b/webserver/observe/config/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.observe helidon-webserver-observe-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-observe-config diff --git a/webserver/observe/health/pom.xml b/webserver/observe/health/pom.xml index d50fb625a15..aacfb65a559 100644 --- a/webserver/observe/health/pom.xml +++ b/webserver/observe/health/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.observe helidon-webserver-observe-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-observe-health diff --git a/webserver/observe/info/pom.xml b/webserver/observe/info/pom.xml index d86cf894f21..6f903ffd07b 100644 --- a/webserver/observe/info/pom.xml +++ b/webserver/observe/info/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.observe helidon-webserver-observe-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-observe-info diff --git a/webserver/observe/log/pom.xml b/webserver/observe/log/pom.xml index 4afeac927aa..57dad132df7 100644 --- a/webserver/observe/log/pom.xml +++ b/webserver/observe/log/pom.xml @@ -22,7 +22,7 @@ io.helidon.webserver.observe helidon-webserver-observe-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-observe-log diff --git a/webserver/observe/metrics/pom.xml b/webserver/observe/metrics/pom.xml index 5bbcfc0e5dc..4e03dead266 100644 --- a/webserver/observe/metrics/pom.xml +++ b/webserver/observe/metrics/pom.xml @@ -22,7 +22,7 @@ io.helidon.webserver.observe helidon-webserver-observe-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-observe-metrics diff --git a/webserver/observe/metrics/src/test/resources/application.yaml b/webserver/observe/metrics/src/test/resources/application.yaml index 204c874771f..4e4e2cc8413 100644 --- a/webserver/observe/metrics/src/test/resources/application.yaml +++ b/webserver/observe/metrics/src/test/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 Oracle and/or its affiliates. +# Copyright (c) 2023, 2024 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,6 +22,6 @@ server: scopes: - name: vendor filter: - include: requests.l.* + include: requests\.l.* key-performance-indicators: extended: true diff --git a/webserver/observe/observe/pom.xml b/webserver/observe/observe/pom.xml index 857048f05a9..9b6d7337558 100644 --- a/webserver/observe/observe/pom.xml +++ b/webserver/observe/observe/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.observe helidon-webserver-observe-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-observe diff --git a/webserver/observe/pom.xml b/webserver/observe/pom.xml index 0a49b8d4528..3cc59af95df 100644 --- a/webserver/observe/pom.xml +++ b/webserver/observe/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.webserver.observe diff --git a/webserver/observe/tracing/pom.xml b/webserver/observe/tracing/pom.xml index 60335c1f22b..022e8682eaa 100644 --- a/webserver/observe/tracing/pom.xml +++ b/webserver/observe/tracing/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.observe helidon-webserver-observe-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-observe-tracing diff --git a/webserver/pom.xml b/webserver/pom.xml index 8e2c0122fda..e7947386427 100644 --- a/webserver/pom.xml +++ b/webserver/pom.xml @@ -24,7 +24,7 @@ io.helidon helidon-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.webserver helidon-webserver-project @@ -46,6 +46,7 @@ testing webserver websocket + concurrency-limits diff --git a/webserver/security/pom.xml b/webserver/security/pom.xml index 9749af0add0..e4ecfdbc542 100644 --- a/webserver/security/pom.xml +++ b/webserver/security/pom.xml @@ -24,7 +24,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-security diff --git a/webserver/security/src/test/resources/security-application.yaml b/webserver/security/src/test/resources/security-application.yaml index 504929aa6fe..f2ae0014db3 100644 --- a/webserver/security/src/test/resources/security-application.yaml +++ b/webserver/security/src/test/resources/security-application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2016, 2023 Oracle and/or its affiliates. +# Copyright (c) 2016, 2024 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -47,6 +47,7 @@ server: - path: "/auditOnly" # method - any # audit all methods (by default GET and HEAD are not audited) + methods: ["get"] audit: true audit-event-type: "unit_test" audit-message-format: "Unit test message format" diff --git a/webserver/service-common/pom.xml b/webserver/service-common/pom.xml index 85a976e676e..968118e0813 100644 --- a/webserver/service-common/pom.xml +++ b/webserver/service-common/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-service-common diff --git a/webserver/sse/pom.xml b/webserver/sse/pom.xml index 405c2431b61..b50f629060c 100644 --- a/webserver/sse/pom.xml +++ b/webserver/sse/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-sse diff --git a/webserver/sse/src/main/java/io/helidon/webserver/sse/DataWriterSseSink.java b/webserver/sse/src/main/java/io/helidon/webserver/sse/DataWriterSseSink.java new file mode 100644 index 00000000000..9457b369291 --- /dev/null +++ b/webserver/sse/src/main/java/io/helidon/webserver/sse/DataWriterSseSink.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.sse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import io.helidon.common.GenericType; +import io.helidon.common.buffers.BufferData; +import io.helidon.common.media.type.MediaType; +import io.helidon.common.media.type.MediaTypes; +import io.helidon.http.DateTime; +import io.helidon.http.Header; +import io.helidon.http.HeaderNames; +import io.helidon.http.HttpMediaType; +import io.helidon.http.ServerResponseHeaders; +import io.helidon.http.Status; +import io.helidon.http.WritableHeaders; +import io.helidon.http.media.EntityWriter; +import io.helidon.http.media.MediaContext; +import io.helidon.http.sse.SseEvent; +import io.helidon.webserver.ConnectionContext; +import io.helidon.webserver.ServerConnectionException; +import io.helidon.webserver.http.ServerResponse; +import io.helidon.webserver.http.spi.SinkProviderContext; + +import static io.helidon.http.HeaderValues.CONTENT_TYPE_EVENT_STREAM; +import static io.helidon.http.HeaderValues.create; + +/** + * Implementation of an SSE sink. Emits {@link SseEvent}s. + */ +class DataWriterSseSink implements SseSink { + + /** + * Type of SSE event sinks. + */ + public static final GenericType TYPE = GenericType.create(DataWriterSseSink.class); + + private static final Header CACHE_NO_CACHE_ONLY = create(HeaderNames.CACHE_CONTROL, "no-cache"); + private static final byte[] SSE_NL = "\n".getBytes(StandardCharsets.UTF_8); + private static final byte[] SSE_ID = "id:".getBytes(StandardCharsets.UTF_8); + private static final byte[] SSE_DATA = "data:".getBytes(StandardCharsets.UTF_8); + private static final byte[] SSE_EVENT = "event:".getBytes(StandardCharsets.UTF_8); + private static final byte[] SSE_COMMENT = ":".getBytes(StandardCharsets.UTF_8); + private static final byte[] OK_200 = "HTTP/1.1 200 OK\r\n".getBytes(StandardCharsets.UTF_8); + private static final byte[] DATE = "Date: ".getBytes(StandardCharsets.UTF_8); + private static final WritableHeaders EMPTY_HEADERS = WritableHeaders.create(); + + private final ServerResponse response; + private final ConnectionContext ctx; + private final MediaContext mediaContext; + private final Runnable closeRunnable; + + DataWriterSseSink(SinkProviderContext context) { + this.response = context.serverResponse(); + this.ctx = context.connectionContext(); + this.mediaContext = ctx.listenerContext().mediaContext(); + this.closeRunnable = context.closeRunnable(); + writeStatusAndHeaders(); + } + + @Override + public DataWriterSseSink emit(SseEvent sseEvent) { + BufferData bufferData = BufferData.growing(512); + + Optional comment = sseEvent.comment(); + if (comment.isPresent()) { + bufferData.write(SSE_COMMENT); + bufferData.write(comment.get().getBytes(StandardCharsets.UTF_8)); + bufferData.write(SSE_NL); + } + Optional id = sseEvent.id(); + if (id.isPresent()) { + bufferData.write(SSE_ID); + bufferData.write(id.get().getBytes(StandardCharsets.UTF_8)); + bufferData.write(SSE_NL); + } + Optional name = sseEvent.name(); + if (name.isPresent()) { + bufferData.write(SSE_EVENT); + bufferData.write(name.get().getBytes(StandardCharsets.UTF_8)); + bufferData.write(SSE_NL); + } + Object data = sseEvent.data(); + if (data != null) { + bufferData.write(SSE_DATA); + byte[] bytes = serializeData(data, sseEvent.mediaType().orElse(MediaTypes.TEXT_PLAIN)); + bufferData.write(bytes); + bufferData.write(SSE_NL); + } + bufferData.write(SSE_NL); + + // write event to the network + ctx.dataWriter().writeNow(bufferData); + return this; + } + + @Override + public void close() { + closeRunnable.run(); + ctx.serverSocket().close(); + } + + void writeStatusAndHeaders() { + ServerResponseHeaders headers = response.headers(); + + // verify response has no status or content type + HttpMediaType ct = headers.contentType().orElse(null); + if (response.status().code() != Status.OK_200.code() + || ct != null && !CONTENT_TYPE_EVENT_STREAM.values().equals(ct.mediaType().text())) { + throw new IllegalStateException("ServerResponse instance cannot be used to create SseSink"); + } + + // start writing status line + BufferData buffer = BufferData.growing(256); + buffer.write(OK_200); + + // serialize a date header if not included + if (!headers.contains(HeaderNames.DATE)) { + buffer.write(DATE); + byte[] dateBytes = DateTime.http1Bytes(); + buffer.write(dateBytes); + } + + // set up and write headers + if (ct == null) { + headers.add(CONTENT_TYPE_EVENT_STREAM); + } + headers.set(CACHE_NO_CACHE_ONLY); + for (Header header : headers) { + header.writeHttp1Header(buffer); + } + + // complete heading + buffer.write('\r'); // "\r\n" - empty line after headers + buffer.write('\n'); + + // write response heading to the network + ctx.dataWriter().writeNow(buffer); + } + + private byte[] serializeData(Object object, MediaType mediaType) { + if (object instanceof byte[] bytes) { + return bytes; + } else if (mediaContext != null) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + if (object instanceof String str && mediaType.equals(MediaTypes.TEXT_PLAIN)) { + EntityWriter writer = mediaContext.writer(GenericType.STRING, EMPTY_HEADERS, EMPTY_HEADERS); + writer.write(GenericType.STRING, str, baos, EMPTY_HEADERS, EMPTY_HEADERS); + } else { + GenericType type = GenericType.create(object); + WritableHeaders resHeaders = WritableHeaders.create(); + resHeaders.set(HeaderNames.CONTENT_TYPE, mediaType.text()); + EntityWriter writer = mediaContext.writer(type, EMPTY_HEADERS, resHeaders); + writer.write(type, object, baos, EMPTY_HEADERS, resHeaders); + } + return baos.toByteArray(); + } catch (IOException e) { + throw new ServerConnectionException("Failed to write SSE event", e); + + } + } + throw new IllegalStateException("Unable to serialize SSE event without a media context"); + } +} diff --git a/webserver/sse/src/main/java/io/helidon/webserver/sse/OutputStreamSseSink.java b/webserver/sse/src/main/java/io/helidon/webserver/sse/OutputStreamSseSink.java new file mode 100644 index 00000000000..9b6db97623f --- /dev/null +++ b/webserver/sse/src/main/java/io/helidon/webserver/sse/OutputStreamSseSink.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.sse; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.function.BiConsumer; + +import io.helidon.common.GenericType; +import io.helidon.common.media.type.MediaType; +import io.helidon.common.media.type.MediaTypes; +import io.helidon.http.HttpMediaType; +import io.helidon.http.Status; +import io.helidon.http.sse.SseEvent; +import io.helidon.webserver.http.ServerResponse; + +import static io.helidon.http.HeaderValues.CONTENT_TYPE_EVENT_STREAM; + +/** + * Deprecated implementation of an SSE sink. Emits {@link SseEvent}s. + * + * @deprecated Should use {@link io.helidon.webserver.sse.DataWriterSseSink}. + */ +@Deprecated(since = "4.1.2", forRemoval = true) +class OutputStreamSseSink implements SseSink { + + /** + * Type of SSE event sinks. + */ + public static final GenericType TYPE = GenericType.create(DataWriterSseSink.class); + + private static final byte[] SSE_NL = "\n".getBytes(StandardCharsets.UTF_8); + private static final byte[] SSE_ID = "id:".getBytes(StandardCharsets.UTF_8); + private static final byte[] SSE_DATA = "data:".getBytes(StandardCharsets.UTF_8); + private static final byte[] SSE_EVENT = "event:".getBytes(StandardCharsets.UTF_8); + private static final byte[] SSE_COMMENT = ":".getBytes(StandardCharsets.UTF_8); + + private final BiConsumer eventConsumer; + private final Runnable closeRunnable; + private final OutputStream outputStream; + + OutputStreamSseSink(ServerResponse serverResponse, BiConsumer eventConsumer, Runnable closeRunnable) { + // Verify response has no status or content type + HttpMediaType ct = serverResponse.headers().contentType().orElse(null); + if (serverResponse.status().code() != Status.OK_200.code() + || ct != null && !CONTENT_TYPE_EVENT_STREAM.values().equals(ct.mediaType().text())) { + throw new IllegalStateException("ServerResponse instance cannot be used to create SseResponse"); + } + + // Ensure content type set for SSE + if (ct == null) { + serverResponse.headers().add(CONTENT_TYPE_EVENT_STREAM); + } + + this.outputStream = serverResponse.outputStream(); + this.eventConsumer = eventConsumer; + this.closeRunnable = closeRunnable; + } + + @Override + public OutputStreamSseSink emit(SseEvent sseEvent) { + try { + Optional comment = sseEvent.comment(); + if (comment.isPresent()) { + outputStream.write(SSE_COMMENT); + outputStream.write(comment.get().getBytes(StandardCharsets.UTF_8)); + outputStream.write(SSE_NL); + } + Optional id = sseEvent.id(); + if (id.isPresent()) { + outputStream.write(SSE_ID); + outputStream.write(id.get().getBytes(StandardCharsets.UTF_8)); + outputStream.write(SSE_NL); + } + Optional name = sseEvent.name(); + if (name.isPresent()) { + outputStream.write(SSE_EVENT); + outputStream.write(name.get().getBytes(StandardCharsets.UTF_8)); + outputStream.write(SSE_NL); + } + Object data = sseEvent.data(); + if (data != SseEvent.NO_DATA) { + outputStream.write(SSE_DATA); + eventConsumer.accept(data, sseEvent.mediaType().orElse(MediaTypes.TEXT_PLAIN)); + outputStream.write(SSE_NL); + } + outputStream.write(SSE_NL); + outputStream.flush(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return this; + } + + @Override + public void close() { + closeRunnable.run(); + } +} diff --git a/webserver/sse/src/main/java/io/helidon/webserver/sse/SseSink.java b/webserver/sse/src/main/java/io/helidon/webserver/sse/SseSink.java index 8cae4685a3e..aa89e5110c7 100644 --- a/webserver/sse/src/main/java/io/helidon/webserver/sse/SseSink.java +++ b/webserver/sse/src/main/java/io/helidon/webserver/sse/SseSink.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024 Oracle and/or its affiliates. + * Copyright (c) 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,102 +13,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.webserver.sse; -import java.io.IOException; -import java.io.OutputStream; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.util.Optional; -import java.util.function.BiConsumer; - import io.helidon.common.GenericType; -import io.helidon.common.media.type.MediaType; -import io.helidon.common.media.type.MediaTypes; -import io.helidon.http.HttpMediaType; -import io.helidon.http.Status; import io.helidon.http.sse.SseEvent; -import io.helidon.webserver.http.ServerResponse; import io.helidon.webserver.http.spi.Sink; -import static io.helidon.http.HeaderValues.CONTENT_TYPE_EVENT_STREAM; - /** - * Implementation of an SSE sink. Emits {@link SseEvent}s. + * A sink for SSE events. */ -public class SseSink implements Sink { +public interface SseSink extends Sink { /** * Type of SSE event sinks. */ - public static final GenericType TYPE = GenericType.create(SseSink.class); - - private static final byte[] SSE_NL = "\n".getBytes(StandardCharsets.UTF_8); - private static final byte[] SSE_ID = "id:".getBytes(StandardCharsets.UTF_8); - private static final byte[] SSE_DATA = "data:".getBytes(StandardCharsets.UTF_8); - private static final byte[] SSE_EVENT = "event:".getBytes(StandardCharsets.UTF_8); - private static final byte[] SSE_COMMENT = ":".getBytes(StandardCharsets.UTF_8); - - private final BiConsumer eventConsumer; - private final Runnable closeRunnable; - private final OutputStream outputStream; - - SseSink(ServerResponse serverResponse, BiConsumer eventConsumer, Runnable closeRunnable) { - // Verify response has no status or content type - HttpMediaType ct = serverResponse.headers().contentType().orElse(null); - if (serverResponse.status().code() != Status.OK_200.code() - || ct != null && !CONTENT_TYPE_EVENT_STREAM.values().equals(ct.mediaType().text())) { - throw new IllegalStateException("ServerResponse instance cannot be used to create SseResponse"); - } - - // Ensure content type set for SSE - if (ct == null) { - serverResponse.headers().add(CONTENT_TYPE_EVENT_STREAM); - } - - this.outputStream = serverResponse.outputStream(); - this.eventConsumer = eventConsumer; - this.closeRunnable = closeRunnable; - } + GenericType TYPE = GenericType.create(SseSink.class); + /** + * Emits an event using to the sink. + * + * @param event the event to emit + * @return this sink + */ @Override - public SseSink emit(SseEvent sseEvent) { - try { - Optional comment = sseEvent.comment(); - if (comment.isPresent()) { - outputStream.write(SSE_COMMENT); - outputStream.write(comment.get().getBytes(StandardCharsets.UTF_8)); - outputStream.write(SSE_NL); - } - Optional id = sseEvent.id(); - if (id.isPresent()) { - outputStream.write(SSE_ID); - outputStream.write(id.get().getBytes(StandardCharsets.UTF_8)); - outputStream.write(SSE_NL); - } - Optional name = sseEvent.name(); - if (name.isPresent()) { - outputStream.write(SSE_EVENT); - outputStream.write(name.get().getBytes(StandardCharsets.UTF_8)); - outputStream.write(SSE_NL); - } - Object data = sseEvent.data(); - if (data != SseEvent.NO_DATA) { - outputStream.write(SSE_DATA); - eventConsumer.accept(data, sseEvent.mediaType().orElse(MediaTypes.TEXT_PLAIN)); - outputStream.write(SSE_NL); - } - outputStream.write(SSE_NL); - outputStream.flush(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return this; - } + SseSink emit(SseEvent event); + /** + * Close SSE sink. + */ @Override - public void close() { - closeRunnable.run(); - } + void close(); } diff --git a/webserver/sse/src/main/java/io/helidon/webserver/sse/SseSinkProvider.java b/webserver/sse/src/main/java/io/helidon/webserver/sse/SseSinkProvider.java index 466a1d5d126..f83625fc37f 100644 --- a/webserver/sse/src/main/java/io/helidon/webserver/sse/SseSinkProvider.java +++ b/webserver/sse/src/main/java/io/helidon/webserver/sse/SseSinkProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,9 +26,12 @@ import io.helidon.webserver.http.ServerResponse; import io.helidon.webserver.http.spi.Sink; import io.helidon.webserver.http.spi.SinkProvider; +import io.helidon.webserver.http.spi.SinkProviderContext; /** * Sink provider for SSE type. + * + * @see io.helidon.webserver.http.spi.SinkProvider */ public class SseSinkProvider implements SinkProvider { @@ -37,10 +40,34 @@ public boolean supports(GenericType> type, ServerRequest reque return SseSink.TYPE.equals(type) && request.headers().isAccepted(MediaTypes.TEXT_EVENT_STREAM); } + /** + * Creates a Sink for SSE events. + * + * @param context the context + * @return newly created sink + * @param type of sink + */ @Override @SuppressWarnings("unchecked") - public > X create(ServerResponse response, BiConsumer eventConsumer, - Runnable closeRunnable) { - return (X) new SseSink(response, eventConsumer, closeRunnable); + public > X create(SinkProviderContext context) { + return (X) new DataWriterSseSink(context); + } + + /** + * Creates a Sink for SSE events. + * + * @param response the HTTP response + * @param eventConsumer an event consumer + * @param closeRunnable a runnable to call on close + * @param type of sink + * @return newly created sink + * @deprecated replaced by {@link #create(SinkProviderContext)} + */ + @Override + @Deprecated(since = "4.1.2", forRemoval = true) + public > X create(ServerResponse response, + BiConsumer eventConsumer, + Runnable closeRunnable) { + return (X) new OutputStreamSseSink(response, eventConsumer, closeRunnable); } } diff --git a/webserver/sse/src/main/java/module-info.java b/webserver/sse/src/main/java/module-info.java index 571dbf8ebf3..5bd5d8ab346 100644 --- a/webserver/sse/src/main/java/module-info.java +++ b/webserver/sse/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ requires static io.helidon.common.features.api; requires transitive io.helidon.common; + requires transitive io.helidon.common.socket; requires transitive io.helidon.http.sse; requires transitive io.helidon.webserver; diff --git a/webserver/static-content/pom.xml b/webserver/static-content/pom.xml index 9a6cd5c0fe6..4ffa8e89a19 100644 --- a/webserver/static-content/pom.xml +++ b/webserver/static-content/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-static-content diff --git a/webserver/testing/junit5/grpc/pom.xml b/webserver/testing/junit5/grpc/pom.xml index 5062e56f54e..acf17b963ea 100644 --- a/webserver/testing/junit5/grpc/pom.xml +++ b/webserver/testing/junit5/grpc/pom.xml @@ -24,7 +24,7 @@ io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-testing-junit5-grpc diff --git a/webserver/testing/junit5/http2/pom.xml b/webserver/testing/junit5/http2/pom.xml index af23ead0463..2e110bf7cb2 100644 --- a/webserver/testing/junit5/http2/pom.xml +++ b/webserver/testing/junit5/http2/pom.xml @@ -24,7 +24,7 @@ io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-testing-junit5-http2 diff --git a/webserver/testing/junit5/junit5/pom.xml b/webserver/testing/junit5/junit5/pom.xml index 53f4b20319d..35895677f8b 100644 --- a/webserver/testing/junit5/junit5/pom.xml +++ b/webserver/testing/junit5/junit5/pom.xml @@ -24,7 +24,7 @@ io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-testing-junit5 Helidon WebServer Testing JUnit5 diff --git a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/DirectClientServerContext.java b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/DirectClientServerContext.java index 03b59ad0f31..e74626ebf81 100644 --- a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/DirectClientServerContext.java +++ b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/DirectClientServerContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -138,4 +138,9 @@ public DirectHandlers directHandlers() { public ListenerConfig config() { return listenerConfiguration; } + + @Override + public HelidonSocket serverSocket() { + return serverSocket; + } } diff --git a/webserver/testing/junit5/pom.xml b/webserver/testing/junit5/pom.xml index cbd079a5982..13be2dc8236 100644 --- a/webserver/testing/junit5/pom.xml +++ b/webserver/testing/junit5/pom.xml @@ -24,7 +24,7 @@ io.helidon.webserver.testing helidon-webserver-testing-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.webserver.testing.junit5 diff --git a/webserver/testing/junit5/websocket/pom.xml b/webserver/testing/junit5/websocket/pom.xml index 0eb67b59e54..651834d9540 100644 --- a/webserver/testing/junit5/websocket/pom.xml +++ b/webserver/testing/junit5/websocket/pom.xml @@ -24,7 +24,7 @@ io.helidon.webserver.testing.junit5 helidon-webserver-testing-junit5-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-testing-junit5-websocket diff --git a/webserver/testing/pom.xml b/webserver/testing/pom.xml index 61e89272220..48aa9b65e96 100644 --- a/webserver/testing/pom.xml +++ b/webserver/testing/pom.xml @@ -24,7 +24,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.webserver.testing diff --git a/webserver/tests/access-log/pom.xml b/webserver/tests/access-log/pom.xml index f41d471b749..858077ab0fb 100644 --- a/webserver/tests/access-log/pom.xml +++ b/webserver/tests/access-log/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-access-log diff --git a/webserver/tests/gh2631/pom.xml b/webserver/tests/gh2631/pom.xml index 02e32164d63..a4046445a43 100644 --- a/webserver/tests/gh2631/pom.xml +++ b/webserver/tests/gh2631/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-gh2631 diff --git a/webserver/tests/grpc/pom.xml b/webserver/tests/grpc/pom.xml index 4df3e1137f1..750e843612f 100644 --- a/webserver/tests/grpc/pom.xml +++ b/webserver/tests/grpc/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-grpc diff --git a/webserver/tests/http2/pom.xml b/webserver/tests/http2/pom.xml index b26acc12dbf..e86c0bec124 100644 --- a/webserver/tests/http2/pom.xml +++ b/webserver/tests/http2/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-http2 diff --git a/webserver/tests/imperative/pom.xml b/webserver/tests/imperative/pom.xml index be6c65fa4fb..911093a29e4 100644 --- a/webserver/tests/imperative/pom.xml +++ b/webserver/tests/imperative/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-imperative diff --git a/webserver/tests/mtls/pom.xml b/webserver/tests/mtls/pom.xml index 908c151203e..bdd422de680 100644 --- a/webserver/tests/mtls/pom.xml +++ b/webserver/tests/mtls/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-mtls diff --git a/webserver/tests/observe/health/pom.xml b/webserver/tests/observe/health/pom.xml index 46d2754611f..f5dfc2c4f68 100644 --- a/webserver/tests/observe/health/pom.xml +++ b/webserver/tests/observe/health/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests.observe helidon-webserver-tests-observe-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-observe-tests-health diff --git a/webserver/tests/observe/observe/pom.xml b/webserver/tests/observe/observe/pom.xml index 4ed80f4fa85..dfe39f99e15 100644 --- a/webserver/tests/observe/observe/pom.xml +++ b/webserver/tests/observe/observe/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests.observe helidon-webserver-tests-observe-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-observe-tests-observe diff --git a/webserver/tests/observe/pom.xml b/webserver/tests/observe/pom.xml index a83bd76e2e2..7f391910409 100644 --- a/webserver/tests/observe/pom.xml +++ b/webserver/tests/observe/pom.xml @@ -24,7 +24,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.webserver.tests.observe diff --git a/webserver/tests/observe/security/pom.xml b/webserver/tests/observe/security/pom.xml index 6191ba53f6c..7532c37b9c8 100644 --- a/webserver/tests/observe/security/pom.xml +++ b/webserver/tests/observe/security/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests.observe helidon-webserver-tests-observe-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-observe-tests-security diff --git a/webserver/tests/observe/weighted/pom.xml b/webserver/tests/observe/weighted/pom.xml index 5a638191adb..1dc08e7479b 100644 --- a/webserver/tests/observe/weighted/pom.xml +++ b/webserver/tests/observe/weighted/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests.observe helidon-webserver-tests-observe-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-observe-weighted diff --git a/webserver/tests/pom.xml b/webserver/tests/pom.xml index 21e3340e0a0..8e63e45de33 100644 --- a/webserver/tests/pom.xml +++ b/webserver/tests/pom.xml @@ -24,7 +24,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT io.helidon.webserver.tests diff --git a/webserver/tests/requested-uri-path-gh8818/pom.xml b/webserver/tests/requested-uri-path-gh8818/pom.xml index 426ac262ffb..5ca4b5cc391 100644 --- a/webserver/tests/requested-uri-path-gh8818/pom.xml +++ b/webserver/tests/requested-uri-path-gh8818/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-requested-uri-path-gh8818 diff --git a/webserver/tests/resource-limits/pom.xml b/webserver/tests/resource-limits/pom.xml index 17ea031f950..5763952bf8a 100644 --- a/webserver/tests/resource-limits/pom.xml +++ b/webserver/tests/resource-limits/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-resource-limits diff --git a/webserver/tests/sse/pom.xml b/webserver/tests/sse/pom.xml index 118c80fb763..7908dfa18af 100644 --- a/webserver/tests/sse/pom.xml +++ b/webserver/tests/sse/pom.xml @@ -21,18 +21,25 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-sse Helidon WebServer Tests SSE WebServer SSE tests + + io.helidon.webserver.tests.sse.Main + + + + io.helidon.webserver + helidon-webserver + io.helidon.webserver helidon-webserver-sse - test io.helidon.webclient @@ -70,4 +77,51 @@ test + + + + + org.apache.maven.plugins + maven-resources-plugin + ${version.plugin.resources} + + + org.apache.maven.plugins + maven-dependency-plugin + ${version.plugin.dependency} + + + copy-libs + prepare-package + + copy-dependencies + + + ${project.build.directory}/libs + false + false + true + true + runtime + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${version.plugin.jar} + + + + true + libs + ${mainClass} + false + + + + + + diff --git a/webserver/tests/sse/src/main/java/io/helidon/webserver/tests/sse/Main.java b/webserver/tests/sse/src/main/java/io/helidon/webserver/tests/sse/Main.java new file mode 100644 index 00000000000..65de0bfac5d --- /dev/null +++ b/webserver/tests/sse/src/main/java/io/helidon/webserver/tests/sse/Main.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tests.sse; + +import java.util.stream.IntStream; + +import io.helidon.http.sse.SseEvent; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import io.helidon.webserver.sse.SseSink; + +/** + * Simple SSE server that can be used to manually test interop with other + * clients such as Postman. + */ +public class Main { + + static void sse(ServerRequest req, ServerResponse res) { + try (SseSink sseSink = res.sink(SseSink.TYPE)) { + IntStream.range(0, 1000).forEach(i -> sseSink.emit(SseEvent.create("hello world " + i))); + } + } + + public static void main(String[] args) { + WebServer.builder() + .port(8080) + .routing(HttpRouting.builder().get("/sse", Main::sse)) + .build() + .start(); + } +} diff --git a/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SimpleSseClient.java b/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SimpleSseClient.java new file mode 100644 index 00000000000..967b0552157 --- /dev/null +++ b/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SimpleSseClient.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.webserver.tests.sse; + +import java.io.BufferedReader; +import java.io.IOException; +import java.time.Duration; +import java.util.Collections; + +import io.helidon.common.testing.http.junit5.SocketHttpClient; +import io.helidon.http.Method; + +class SimpleSseClient implements AutoCloseable { + + public enum State { + DISCONNECTED, + CONNECTED, + HEADERS_READ, + ERROR + } + + private State state = State.DISCONNECTED; + private BufferedReader reader; + private final String path; + private final SocketHttpClient client; + + public static SimpleSseClient create(int port, String path) { + return create("localhost", port, path, Duration.ofSeconds(10)); + } + + public static SimpleSseClient create(int port, String path, Duration timeout) { + return create("localhost", port, path, timeout); + } + + public static SimpleSseClient create(String host, int port, String path, Duration timeout) { + return new SimpleSseClient("localhost", port, path, timeout); + } + + private SimpleSseClient(String host, int port, String path, Duration timeout) { + this.path = path; + this.client = SocketHttpClient.create(host, port, timeout); + } + + public String nextEvent() { + ensureConnected(); + ensureHeadersRead(); + + try { + String line; + StringBuilder event = new StringBuilder(); + while ((line = reader.readLine()) != null) { + if (line.isEmpty()) { + return event.toString(); + } + if (!event.isEmpty()) { + event.append("\n"); + } + event.append(line); + } + if (event.isEmpty()) { + return null; + } + state = State.ERROR; + throw new RuntimeException("Unable to parse response"); + } catch (IOException e) { + state = State.ERROR; + throw new RuntimeException(e); + } + } + + @Override + public void close() throws Exception { + client.close(); + state = State.DISCONNECTED; + + } + + public State state() { + return state; + } + + private void ensureConnected() { + if (state == State.DISCONNECTED) { + client.request(Method.GET.toString(), + path, + "HTTP/1.1", + "localhost", + Collections.emptyList(), + null); + reader = client.socketReader(); + state = State.CONNECTED; + } + } + + private void ensureHeadersRead() { + if (state == State.CONNECTED) { + try { + String line; + while ((line = reader.readLine()) != null) { + if (line.isEmpty()) { + state = State.HEADERS_READ; + return; + } + line = line.toLowerCase(); + if (line.contains("http/1.1") && !line.contains("200")) { + throw new RuntimeException("Invalid status code in response"); + } else if (line.contains("content-type") && !line.contains("text/event-stream")) { + throw new RuntimeException("Invalid content-type in response"); + } + } + state = State.ERROR; + throw new RuntimeException("Unable to parse response"); + } catch (IOException e) { + state = State.ERROR; + throw new RuntimeException(e); + } + } + } +} diff --git a/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseBaseTest.java b/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseBaseTest.java index c25552ecb46..8254f8e4e51 100644 --- a/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseBaseTest.java +++ b/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseBaseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,34 @@ package io.helidon.webserver.tests.sse; import io.helidon.http.sse.SseEvent; -import io.helidon.webserver.sse.SseSink; +import io.helidon.webserver.WebServer; import io.helidon.webserver.http.ServerRequest; import io.helidon.webserver.http.ServerResponse; +import io.helidon.webserver.sse.SseSink; + import jakarta.json.Json; import jakarta.json.JsonObject; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + class SseBaseTest { + private final WebServer webServer; + + SseBaseTest() { + this.webServer = null; + } + + SseBaseTest(WebServer webServer) { + this.webServer = webServer; + } + + protected WebServer webServer() { + return webServer; + } + static void sseString1(ServerRequest req, ServerResponse res) { try (SseSink sseSink = res.sink(SseSink.TYPE)) { sseSink.emit(SseEvent.create("hello")) @@ -96,4 +116,14 @@ static void sseIdComment(ServerRequest req, ServerResponse res) { sseSink.emit(event); } } + + protected void testSse(String path, String... events) throws Exception { + assert webServer != null; + try (SimpleSseClient sseClient = SimpleSseClient.create(webServer.port(), path)) { + for (String e : events) { + assertThat(sseClient.nextEvent(), is(e)); + } + assertThat(sseClient.nextEvent(), is(nullValue())); + } + } } diff --git a/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseClientTest.java b/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseClientTest.java index a812b102841..f039353cca4 100644 --- a/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseClientTest.java +++ b/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseClientTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import io.helidon.webclient.http1.Http1Client; import io.helidon.webclient.http1.Http1ClientResponse; import io.helidon.webclient.sse.SseSource; +import io.helidon.webserver.WebServer; import io.helidon.webserver.http.HttpRules; import io.helidon.webserver.http.ServerRequest; import io.helidon.webserver.http.ServerResponse; @@ -44,7 +45,8 @@ class SseClientTest extends SseBaseTest { private final Http1Client client; - SseClientTest(Http1Client client) { + SseClientTest(WebServer webServer, Http1Client client) { + super(webServer); this.client = client; } diff --git a/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseServerMediaTest.java b/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseServerMediaTest.java index 0ee68a6bc8c..b22f446530d 100644 --- a/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseServerMediaTest.java +++ b/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseServerMediaTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ import io.helidon.common.config.Config; import io.helidon.http.Headers; import io.helidon.http.HttpMediaType; -import io.helidon.http.Status; import io.helidon.http.WritableHeaders; import io.helidon.http.media.EntityWriter; import io.helidon.http.media.MediaSupport; @@ -34,7 +33,7 @@ import io.helidon.http.media.spi.MediaSupportProvider; import io.helidon.http.sse.SseEvent; import io.helidon.webclient.http1.Http1Client; -import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServer; import io.helidon.webserver.http.HttpRules; import io.helidon.webserver.http.ServerRequest; import io.helidon.webserver.http.ServerResponse; @@ -44,23 +43,20 @@ import org.junit.jupiter.api.Test; -import static io.helidon.http.HeaderValues.ACCEPT_EVENT_STREAM; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - /** * Test that shows how to serialize an individual SSE event using a custom * {@link io.helidon.http.media.spi.MediaSupportProvider} and a user-defined * media type. Each SSE event can be given a different media type. */ @ServerTest -class SseServerMediaTest { +class SseServerMediaTest extends SseBaseTest { private static final HttpMediaType MY_PLAIN_TEXT = HttpMediaType.create("text/my_plain"); private final Http1Client client; - SseServerMediaTest(Http1Client client) { + SseServerMediaTest(WebServer webServer, Http1Client client) { + super(webServer); this.client = client; } @@ -70,8 +66,8 @@ static void routing(HttpRules rules) { } @Test - void testSseJson() { - testSse("/sse", "data:HELLO\n\ndata:world\n\n"); + void testSseJson() throws Exception { + testSse("/sse", "data:HELLO", "data:world"); } private static void sse(ServerRequest req, ServerResponse res) { @@ -81,13 +77,6 @@ private static void sse(ServerRequest req, ServerResponse res) { } } - private void testSse(String path, String result) { - try (Http1ClientResponse response = client.get(path).header(ACCEPT_EVENT_STREAM).request()) { - assertThat(response.status(), is(Status.OK_200)); - assertThat(response.as(String.class), is(result)); - } - } - @SuppressWarnings("unchecked") public static class MyStringSupport extends StringSupport { @@ -143,6 +132,10 @@ private void write(String toWrite, } } + /** + * Provider for {@link io.helidon.webserver.tests.sse.SseServerMediaTest.MyStringSupport}, + * loaded as a service. + */ public static class MyStringSupportProvider implements MediaSupportProvider, Weighted { @Override diff --git a/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseServerTest.java b/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseServerTest.java index cf49ddbe2d8..81a71df14a4 100644 --- a/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseServerTest.java +++ b/webserver/tests/sse/src/test/java/io/helidon/webserver/tests/sse/SseServerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,13 @@ import io.helidon.http.Status; import io.helidon.webclient.http1.Http1Client; import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webserver.WebServer; import io.helidon.webserver.http.HttpRules; import io.helidon.webserver.testing.junit5.ServerTest; import io.helidon.webserver.testing.junit5.SetUpRoute; import org.junit.jupiter.api.Test; -import static io.helidon.http.HeaderValues.ACCEPT_EVENT_STREAM; import static io.helidon.http.HeaderValues.ACCEPT_JSON; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -33,10 +33,8 @@ @ServerTest class SseServerTest extends SseBaseTest { - private final Http1Client client; - - SseServerTest(Http1Client client) { - this.client = client; + SseServerTest(WebServer webServer) { + super(webServer); } @SetUpRoute @@ -50,49 +48,43 @@ static void routing(HttpRules rules) { } @Test - void testSseString1() { - testSse("/sseString1", "data:hello\n\ndata:world\n\n"); + void testSseString1() throws Exception { + testSse("/sseString1", "data:hello", "data:world"); } @Test - void testSseString2() { - testSse("/sseString2", "data:1\n\ndata:2\n\ndata:3\n\n"); + void testSseString2() throws Exception { + testSse("/sseString2", "data:1", "data:2", "data:3"); } @Test - void testSseJson1() { - testSse("/sseJson1", "data:{\"hello\":\"world\"}\n\n"); + void testSseJson1() throws Exception { + testSse("/sseJson1", "data:{\"hello\":\"world\"}"); } @Test - void testSseJson2() { - testSse("/sseJson2", "data:{\"hello\":\"world\"}\n\n"); + void testSseJson2() throws Exception { + testSse("/sseJson2", "data:{\"hello\":\"world\"}"); } @Test - void testSseMixed() { - testSse("/sseMixed", - "data:hello\n\ndata:world\n\n" + - "data:{\"hello\":\"world\"}\n\n" + - "data:{\"hello\":\"world\"}\n\n"); + void testSseMixed() throws Exception { + testSse("/sseMixed", "data:hello", "data:world", + "data:{\"hello\":\"world\"}", "data:{\"hello\":\"world\"}"); } @Test - void testIdComment() { - testSse("/sseIdComment", ":This is a comment\nid:1\ndata:hello\n\n"); + void testIdComment() throws Exception { + testSse("/sseIdComment", ":This is a comment\nid:1\ndata:hello"); } @Test void testWrongAcceptType() { + Http1Client client = Http1Client.builder() + .baseUri("http://localhost:" + webServer().port()) + .build(); try (Http1ClientResponse response = client.get("/sseString1").header(ACCEPT_JSON).request()) { assertThat(response.status(), is(Status.NOT_ACCEPTABLE_406)); } } - - private void testSse(String path, String result) { - try (Http1ClientResponse response = client.get(path).header(ACCEPT_EVENT_STREAM).request()) { - assertThat(response.status(), is(Status.OK_200)); - assertThat(response.as(String.class), is(result)); - } - } } diff --git a/webserver/tests/static-content/pom.xml b/webserver/tests/static-content/pom.xml index 60e0a4e468f..f8ff28a0598 100644 --- a/webserver/tests/static-content/pom.xml +++ b/webserver/tests/static-content/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-tests-static-content diff --git a/webserver/tests/upgrade/pom.xml b/webserver/tests/upgrade/pom.xml index b3e91a036c2..8a12ae833cb 100644 --- a/webserver/tests/upgrade/pom.xml +++ b/webserver/tests/upgrade/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-upgrade diff --git a/webserver/tests/webserver/pom.xml b/webserver/tests/webserver/pom.xml index 6948bb5cae9..1d5d5cb8c0e 100644 --- a/webserver/tests/webserver/pom.xml +++ b/webserver/tests/webserver/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-weserver-tests-webserver diff --git a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadHostTest.java b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadHostTest.java new file mode 100644 index 00000000000..99fccc4df27 --- /dev/null +++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadHostTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tests; + +import io.helidon.http.Header; +import io.helidon.http.HeaderValues; +import io.helidon.http.Method; +import io.helidon.http.Status; +import io.helidon.webclient.api.ClientResponseTyped; +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webserver.http.HttpRouting; +import io.helidon.webserver.http1.Http1Route; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpRoute; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +class BadHostTest { + private static final Header BAD_HOST_HEADER = HeaderValues.create("Host", "localhost:808a"); + + private final Http1Client client; + + BadHostTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + builder.route(Http1Route.route(Method.GET, + "/", + (req, res) -> res.send(req.requestedUri().host()))); + } + + @Test + void testOk() { + String response = client.method(Method.GET) + .requestEntity(String.class); + + assertThat(response, is("localhost")); + } + + @Test + void testInvalidRequest() { + ClientResponseTyped response = client.method(Method.GET) + .header(BAD_HOST_HEADER) + .request(String.class); + + assertThat(response.status(), is(Status.BAD_REQUEST_400)); + assertThat(response.entity(), is("Invalid port of the host header: 808a")); + } +} diff --git a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadRequestTest.java b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadRequestTest.java index 59237c6183d..a18675ba4db 100644 --- a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadRequestTest.java +++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BadRequestTest.java @@ -43,6 +43,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; + @ServerTest class BadRequestTest { public static final String CUSTOM_REASON_PHRASE = "Custom-bad-request"; @@ -71,13 +72,10 @@ static void setUpServer(WebServerConfig.Builder builder) { .build()); } - // no need to try with resources when reading as string - @SuppressWarnings("resource") @Test void testOk() { String response = client.method(Method.GET) - .request() - .as(String.class); + .requestEntity(String.class); assertThat(response, is("Hi")); } diff --git a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/HeadersServerTest.java b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/HeadersServerTest.java index ac708821857..22ef244d9df 100644 --- a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/HeadersServerTest.java +++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/HeadersServerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,6 +89,16 @@ static void router(HttpRouting.Builder router) { res.send(DATA); } ); + router.route(GET, "/stream-with-trailers-and-length", + (req, res) -> { + res.header(HeaderNames.TRAILER, TEST_TRAILER_HEADER.name()); + res.header(HeaderNames.CONTENT_LENGTH, String.valueOf(DATA.length())); // must switch to chunked + try (var os = res.outputStream()) { + os.write(DATA.getBytes()); + } + res.trailers().add(TEST_TRAILER_HEADER); + } + ); } @Test @@ -149,6 +159,19 @@ void trailersNoTrailers(WebClient client) { + "response headers have trailer names definition 'Trailer: '")); } + @Test + void streamWithTrailersAndLength(WebClient client) throws IOException { + ClientResponseTyped res = client + .get("/stream-with-trailers-and-length") + .header(HeaderValues.TE_TRAILERS) + .request(InputStream.class); + assertThat(res.headers(), hasHeader(HeaderValues.TRANSFER_ENCODING_CHUNKED)); // trailers need chunked + try (var ins = res.entity()) { + assertThat(ins.readAllBytes(), is(DATA.getBytes())); + } + assertThat(res.trailers(), hasHeader(TEST_TRAILER_HEADER)); + } + private void checkCachedConnection(ClientResponseHeaders h) { if (clientPort == -1) { clientPort = h.get(CLIENT_PORT_HEADER_NAME).asInt().get(); diff --git a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/OutputStreamBufferSizeTest.java b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/OutputStreamBufferSizeTest.java new file mode 100644 index 00000000000..966e1a54549 --- /dev/null +++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/OutputStreamBufferSizeTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.webserver.tests; + +import io.helidon.webclient.http1.Http1Client; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; + +@ServerTest +class OutputStreamBufferSizeTest extends OutputStreamTest { + + OutputStreamBufferSizeTest(Http1Client client) { + super(client); + } + + @SetUpServer + static void setup(WebServerConfig.Builder builder) { + builder.writeBufferSize(0); // should accept buffer size of 0 + } +} diff --git a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/OutputStreamTest.java b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/OutputStreamTest.java index 2411e420644..3733604eda5 100644 --- a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/OutputStreamTest.java +++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/OutputStreamTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,7 +59,7 @@ void verifyAutoFlush() { } } - private static class Service { + protected static class Service { public static void outputStream(ServerRequest req, ServerResponse res) throws IOException { InputStream in = new ByteArrayInputStream("Hello World".getBytes(StandardCharsets.UTF_8)); diff --git a/webserver/tests/websocket/pom.xml b/webserver/tests/websocket/pom.xml index 8ae2b0e0a1c..82342596dcf 100644 --- a/webserver/tests/websocket/pom.xml +++ b/webserver/tests/websocket/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver.tests helidon-webserver-tests-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver-tests-websocket diff --git a/webserver/webserver/pom.xml b/webserver/webserver/pom.xml index cb38365577e..77bb9ada598 100644 --- a/webserver/webserver/pom.xml +++ b/webserver/webserver/pom.xml @@ -21,7 +21,7 @@ io.helidon.webserver helidon-webserver-project - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT helidon-webserver @@ -80,6 +80,10 @@ io.helidon.common.features helidon-common-features + + io.helidon.common.concurrency + helidon-common-concurrency-limits +