From 833ef1d32d87d6ca7060eccf92f289457bdd0d8f Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Thu, 8 Aug 2024 16:39:26 +0200 Subject: [PATCH 01/22] Upgrade to Jandex 3.2.2 (cherry picked from commit 20798d90086ea1f90a66d6e7583b272bf1f04722) --- bom/application/pom.xml | 2 +- build-parent/pom.xml | 2 +- independent-projects/arc/pom.xml | 2 +- independent-projects/bootstrap/pom.xml | 2 +- independent-projects/junit5-virtual-threads/pom.xml | 3 +-- independent-projects/qute/pom.xml | 2 +- independent-projects/resteasy-reactive/pom.xml | 2 +- independent-projects/tools/pom.xml | 2 +- 8 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 3e6cd23ecad43..00ea1a94d93c0 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -20,7 +20,7 @@ 1.0.19 5.0.0 3.0.2 - 3.2.1 + 3.2.2 1.3.2 1 1.1.7 diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 45dbcdcdb4f0a..44c289d56c94a 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -28,7 +28,7 @@ ${scala-maven-plugin.version} - 3.2.1 + 3.2.2 1.0.0 2.5.13 diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 72af26b14889d..8c44352a59772 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -45,7 +45,7 @@ 2.0.1 1.8.0 - 3.2.1 + 3.2.2 3.6.0.Final 2.6.2 1.6.Final diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index 9cc9c12dd2001..6dc2d58c7e9c9 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -34,7 +34,7 @@ 1.3.2 1 UTF-8 - 3.2.1 + 3.2.2 1.37 diff --git a/independent-projects/junit5-virtual-threads/pom.xml b/independent-projects/junit5-virtual-threads/pom.xml index 9ca7257288417..b70e67e849636 100644 --- a/independent-projects/junit5-virtual-threads/pom.xml +++ b/independent-projects/junit5-virtual-threads/pom.xml @@ -41,10 +41,9 @@ 3.13.0 3.2.1 3.2.5 - 3.2.1 + 3.2.2 2.24.1 1.11.0 - 5.10.3 3.26.3 diff --git a/independent-projects/qute/pom.xml b/independent-projects/qute/pom.xml index 675387eb89086..71300625db83b 100644 --- a/independent-projects/qute/pom.xml +++ b/independent-projects/qute/pom.xml @@ -40,7 +40,7 @@ UTF-8 5.10.3 3.26.3 - 3.2.1 + 3.2.2 1.8.0 3.6.0.Final 2.6.2 diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index c305e3fd84091..e95515cde4167 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -45,7 +45,7 @@ UTF-8 4.1.0 - 3.2.1 + 3.2.2 1.14.11 5.10.3 3.9.8 diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index cad9357939434..925a85d5836e6 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -57,7 +57,7 @@ 5.12.0 ${project.version} 37 - 3.2.1 + 3.2.2 2.0.2 4.2.1 0.0.7 From 92d942fe49fb51deeed8af8e98ed2b85eebd4bec Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 9 Aug 2024 09:27:21 +0200 Subject: [PATCH 02/22] Qute template records: fix the way the canonical constructor is found - also fix the problem with compact record constructor - resolves #42411 (cherry picked from commit c05d0b1894d964820915a87fda9e3045e6ac3853) --- .../qute/deployment/QuteProcessor.java | 9 +- .../deployment/TemplateRecordEnhancer.java | 129 +++++++++--------- .../records/TemplateRecordTest.java | 26 +++- 3 files changed, 93 insertions(+), 71 deletions(-) diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 193fd8f588f78..161ed826f7c73 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -435,9 +435,9 @@ && isNotLocatedByCustomTemplateLocator(locatorPatternsBuildItem.getLocationPatte if (!recordClass.isRecord()) { continue; } - Type[] componentTypes = recordClass.recordComponents().stream().map(RecordComponentInfo::type) - .toArray(Type[]::new); - MethodInfo canonicalConstructor = recordClass.method(MethodDescriptor.INIT, componentTypes); + MethodInfo canonicalConstructor = recordClass.method(MethodDescriptor.INIT, + recordClass.unsortedRecordComponents().stream().map(RecordComponentInfo::type) + .toArray(Type[]::new)); AnnotationInstance checkedTemplateAnnotation = recordClass.declaredAnnotation(Names.CHECKED_TEMPLATE); String fragmentId = getCheckedFragmentId(recordClass, checkedTemplateAnnotation); @@ -510,7 +510,8 @@ && isNotLocatedByCustomTemplateLocator(locatorPatternsBuildItem.getLocationPatte requireTypeSafeExpressions != null ? requireTypeSafeExpressions.asBoolean() : true)); transformers.produce(new BytecodeTransformerBuildItem(recordClass.name().toString(), new TemplateRecordEnhancer(recordInterface, recordClass, templatePath, fragmentId, - canonicalConstructor.parameters(), adaptors.get(recordInterfaceName)))); + canonicalConstructor.descriptor(), canonicalConstructor.parameters(), + adaptors.get(recordInterfaceName)))); } } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRecordEnhancer.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRecordEnhancer.java index b9da1875790d7..9e29ea4a2b710 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRecordEnhancer.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateRecordEnhancer.java @@ -34,15 +34,17 @@ class TemplateRecordEnhancer implements BiFunction parameters; private final CheckedTemplateAdapter adapter; TemplateRecordEnhancer(ClassInfo interfaceToImplement, ClassInfo recordClass, String templateId, String fragmentId, - List parameters, CheckedTemplateAdapter adapter) { + String canonicalConstructorDescriptor, List parameters, CheckedTemplateAdapter adapter) { this.interfaceToImplement = interfaceToImplement; this.recordClassName = recordClass.name().toString(); this.templateId = templateId; this.fragmentId = fragmentId; + this.canonicalConstructorDescriptor = canonicalConstructorDescriptor; this.parameters = parameters; this.adapter = adapter; } @@ -61,7 +63,7 @@ public TemplateRecordClassVisitor(ClassVisitor outputClassVisitor) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor ret = super.visitMethod(access, name, descriptor, signature, exceptions); - if (name.equals(MethodDescriptor.INIT)) { + if (name.equals(MethodDescriptor.INIT) && descriptor.equals(canonicalConstructorDescriptor)) { return new TemplateRecordConstructorVisitor(ret); } return ret; @@ -126,74 +128,69 @@ public TemplateRecordConstructorVisitor(MethodVisitor outputVisitor) { @Override public void visitInsn(int opcode) { - if (opcode != Opcodes.RETURN) { - super.visitInsn(opcode); - } - } + if (opcode == Opcodes.RETURN) { + visitVarInsn(Opcodes.ALOAD, 0); + + MethodDescriptor arcContainer = MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class); + MethodDescriptor arcContainerInstance = MethodDescriptor.ofMethod(ArcContainer.class, "instance", + InstanceHandle.class, Class.class, Annotation[].class); + MethodDescriptor instanceHandleGet = MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class); + MethodDescriptor templateProducerGetInjectableTemplate = MethodDescriptor.ofMethod(TemplateProducer.class, + "getInjectableTemplate", Template.class, String.class); + MethodDescriptor templateInstance = MethodDescriptor.ofMethod(Template.class, "instance", + TemplateInstance.class); + MethodDescriptor templateGetFragment = MethodDescriptor.ofMethod(Template.class, "getFragment", + Template.Fragment.class, String.class); + MethodDescriptor templateInstanceData = MethodDescriptor.ofMethod(TemplateInstance.class, "data", + TemplateInstance.class, String.class, Object.class); + + // Template template = Arc.container().instance(TemplateProducer.class).get().getInjectableTemplate("HelloResource/typedTemplate"); + visitMethodInsn(Opcodes.INVOKESTATIC, arcContainer.getDeclaringClass(), arcContainer.getName(), + arcContainer.getDescriptor(), + false); + visitLdcInsn(org.objectweb.asm.Type.getType(TemplateProducer.class)); + visitLdcInsn(0); + visitTypeInsn(Opcodes.ANEWARRAY, toInternalClassName(Annotation.class)); + invokeInterface(this, arcContainerInstance); + invokeInterface(this, instanceHandleGet); + visitTypeInsn(Opcodes.CHECKCAST, toInternalClassName(TemplateProducer.class)); + visitLdcInsn(templateId); + visitMethodInsn(Opcodes.INVOKEVIRTUAL, templateProducerGetInjectableTemplate.getDeclaringClass(), + templateProducerGetInjectableTemplate.getName(), + templateProducerGetInjectableTemplate.getDescriptor(), false); + if (fragmentId != null) { + // template = template.getFragment(id); + visitLdcInsn(fragmentId); + invokeInterface(this, templateGetFragment); + } + // templateInstance = template.instance(); + invokeInterface(this, templateInstance); - @Override - public void visitEnd() { - visitCode(); - visitVarInsn(Opcodes.ALOAD, 0); - - MethodDescriptor arcContainer = MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class); - MethodDescriptor arcContainerInstance = MethodDescriptor.ofMethod(ArcContainer.class, "instance", - InstanceHandle.class, Class.class, Annotation[].class); - MethodDescriptor instanceHandleGet = MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class); - MethodDescriptor templateProducerGetInjectableTemplate = MethodDescriptor.ofMethod(TemplateProducer.class, - "getInjectableTemplate", Template.class, String.class); - MethodDescriptor templateInstance = MethodDescriptor.ofMethod(Template.class, "instance", TemplateInstance.class); - MethodDescriptor templateGetFragment = MethodDescriptor.ofMethod(Template.class, "getFragment", - Template.Fragment.class, String.class); - MethodDescriptor templateInstanceData = MethodDescriptor.ofMethod(TemplateInstance.class, "data", - TemplateInstance.class, String.class, Object.class); - - // Template template = Arc.container().instance(TemplateProducer.class).get().getInjectableTemplate("HelloResource/typedTemplate"); - visitMethodInsn(Opcodes.INVOKESTATIC, arcContainer.getDeclaringClass(), arcContainer.getName(), - arcContainer.getDescriptor(), - false); - visitLdcInsn(org.objectweb.asm.Type.getType(TemplateProducer.class)); - visitLdcInsn(0); - visitTypeInsn(Opcodes.ANEWARRAY, toInternalClassName(Annotation.class)); - invokeInterface(this, arcContainerInstance); - invokeInterface(this, instanceHandleGet); - visitTypeInsn(Opcodes.CHECKCAST, toInternalClassName(TemplateProducer.class)); - visitLdcInsn(templateId); - visitMethodInsn(Opcodes.INVOKEVIRTUAL, templateProducerGetInjectableTemplate.getDeclaringClass(), - templateProducerGetInjectableTemplate.getName(), - templateProducerGetInjectableTemplate.getDescriptor(), false); - if (fragmentId != null) { - // template = template.getFragment(id); - visitLdcInsn(fragmentId); - invokeInterface(this, templateGetFragment); - } - // templateInstance = template.instance(); - invokeInterface(this, templateInstance); + if (adapter != null) { + adapter.convertTemplateInstance(this); + templateInstanceData = MethodDescriptor.ofMethod(adapter.templateInstanceBinaryName(), "data", + adapter.templateInstanceBinaryName(), String.class, Object.class); + } - if (adapter != null) { - adapter.convertTemplateInstance(this); - templateInstanceData = MethodDescriptor.ofMethod(adapter.templateInstanceBinaryName(), "data", - adapter.templateInstanceBinaryName(), String.class, Object.class); - } + int slot = 1; + for (int i = 0; i < parameters.size(); i++) { + // instance = instance.data("name", value); + Type paramType = parameters.get(i).type(); + visitLdcInsn(parameters.get(i).name()); + visitVarInsn(AsmUtil.getLoadOpcode(paramType), slot); + AsmUtil.boxIfRequired(this, paramType); + invokeInterface(this, templateInstanceData); + slot += AsmUtil.getParameterSize(paramType); + } - int slot = 1; - for (int i = 0; i < parameters.size(); i++) { - // instance = instance.data("name", value); - Type paramType = parameters.get(i).type(); - visitLdcInsn(parameters.get(i).name()); - visitVarInsn(AsmUtil.getLoadOpcode(paramType), slot); - AsmUtil.boxIfRequired(this, paramType); - invokeInterface(this, templateInstanceData); - slot += AsmUtil.getParameterSize(paramType); + FieldDescriptor quteTemplateInstanceDescriptor = FieldDescriptor.of(recordClassName, "qute$templateInstance", + interfaceToImplement.name().toString()); + visitFieldInsn(Opcodes.PUTFIELD, quteTemplateInstanceDescriptor.getDeclaringClass(), + quteTemplateInstanceDescriptor.getName(), quteTemplateInstanceDescriptor.getType()); + super.visitInsn(Opcodes.RETURN); + } else { + super.visitInsn(opcode); } - - FieldDescriptor quteTemplateInstanceDescriptor = FieldDescriptor.of(recordClassName, "qute$templateInstance", - interfaceToImplement.name().toString()); - visitFieldInsn(Opcodes.PUTFIELD, quteTemplateInstanceDescriptor.getDeclaringClass(), - quteTemplateInstanceDescriptor.getName(), quteTemplateInstanceDescriptor.getType()); - super.visitInsn(Opcodes.RETURN); - super.visitEnd(); - visitMaxs(-1, -1); } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/records/TemplateRecordTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/records/TemplateRecordTest.java index e5e9e13b8840a..d3f7e1546d14a 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/records/TemplateRecordTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/records/TemplateRecordTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -34,7 +35,9 @@ public class TemplateRecordTest { .addAsResource(new StringAsset("Hello {name}!"), "templates/hello_world.txt") .addAsResource(new StringAsset("Hello {#fragment id=name}{name}{/fragment}!"), - "templates/hello.txt")); + "templates/hello.txt") + .addAsResource(new StringAsset("{alpha}:{bravo}:{charlie}"), + "templates/TemplateRecordTest/multiParams.txt")); @Inject Engine engine; @@ -69,6 +72,9 @@ public void testTemplateRecords() throws InterruptedException, ExecutionExceptio assertEquals("Hello Lu!", new helloWorld("Lu").render()); assertEquals("Lu", new hello$name("Lu").render()); + + assertEquals("15:true:foo", new multiParams(true, 15, "foo").render()); + assertThrows(IllegalArgumentException.class, () -> new multiParams(false, 50, null)); } record HelloInt(int val) implements TemplateInstance { @@ -82,4 +88,22 @@ record helloWorld(String name) implements TemplateInstance { record hello$name(String name) implements TemplateInstance { } + record multiParams(boolean bravo, int alpha, String charlie) implements TemplateInstance { + + public multiParams { + if (alpha > 20) { + throw new IllegalArgumentException(); + } + } + + public multiParams(String delta) { + this(false, 1, delta); + } + + public multiParams(int alpha, boolean bravo, String charlie) { + this(bravo, alpha, charlie); + throw new IllegalArgumentException(""); + } + } + } From 660cb3fa1ef8f3c3e509f5625fdc18bece3fa171 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 9 Aug 2024 08:43:32 +0300 Subject: [PATCH 03/22] Update javadoc and docs about `@WithTestResource` Closes: #42406 (cherry picked from commit 58c294f19b9410fc3cd92bb4906dd808be2c580f) --- .../main/asciidoc/getting-started-testing.adoc | 9 +++++++-- .../quarkus/test/common/QuarkusTestResource.java | 8 ++++++-- .../io/quarkus/test/common/WithTestResource.java | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 5841b24e68c4d..f7ea25492044f 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1088,11 +1088,16 @@ If you are using Quarkus Security, check out the xref:security-testing.adoc[Test A very common need is to start some services on which your Quarkus application depends, before the Quarkus application starts for testing. To address this need, Quarkus provides `@io.quarkus.test.common.WithTestResource` and `io.quarkus.test.common.QuarkusTestResourceLifecycleManager`. -By simply annotating any test in the test suite with `@WithTestResource`, Quarkus will run the corresponding `QuarkusTestResourceLifecycleManager` before any tests are run. -A test suite is also free to utilize multiple `@WithTestResource` annotations, in which case all the corresponding `QuarkusTestResourceLifecycleManager` objects will be run before the tests. When using multiple test resources they can be started concurrently. For that you need to set `@WithTestResource(parallel = true)`. +When a test annotated with `@WithTestResource`, Quarkus will run the corresponding `QuarkusTestResourceLifecycleManager` before the test. + +IMPORTANT: By default, `@WithTestResource` applies only to the test on which the annotation is placed. Each test that is annotated with `@WithTestResource` will result in the application being re-augmented and restarted +(in a similar fashion as happens in dev-mode when a change is detected) in order to incorporate the settings configured by the annotation. This means that if there are many instances of the annotation used throughout the testsuite, +test execution speed will be impacted by these restarts. NOTE: Test resources are applied for a given test class or custom profile. To activate for all tests you can use `@WithTestResource(restrictToAnnotatedClass = false)`. +NOTE: When using multiple test resources they can be started concurrently. For that you need to set `@WithTestResource(parallel = true)`. + Quarkus provides a few implementations of `QuarkusTestResourceLifecycleManager` out of the box (see `io.quarkus.test.h2.H2DatabaseTestResource` which starts an H2 database, or `io.quarkus.test.kubernetes.client.KubernetesServerTestResource` which starts a mock Kubernetes API server), but it is common to create custom implementations to address specific application needs. Common cases include starting docker containers using https://www.testcontainers.org/[Testcontainers] (an example of which can be found https://github.com/quarkusio/quarkus/blob/main/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java[here]), diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResource.java b/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResource.java index 803fb23abc179..567b1ed21443f 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResource.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResource.java @@ -12,12 +12,12 @@ /** * Used to define a test resource. - * + *

* All {@code QuarkusTestResource} annotations in the test module * are discovered (regardless of the test which contains the annotation) * and their corresponding {@code QuarkusTestResourceLifecycleManager} * started before any test is run. - * + *

* Note that test resources are never restarted when running {@code @Nested} test classes. * * @deprecated Use the new {@link WithTestResource} instead. It will be a long while before this is removed, but better to move @@ -51,6 +51,10 @@ * Whether this annotation should only be enabled if it is placed on the currently running test class or test profile. * Note that this defaults to true for meta-annotations since meta-annotations are only considered * for the current test class or test profile. + *

+ * Note: When this is set to {@code true} (which is the default), the annotation {@code @WithTestResource} will result + * in the application being re-augmented and restarted (in a similar fashion as happens in dev-mode when a change is + * detected). */ boolean restrictToAnnotatedClass() default false; diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/WithTestResource.java b/test-framework/common/src/main/java/io/quarkus/test/common/WithTestResource.java index c44d91cdac803..efd1372f8e396 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/WithTestResource.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/WithTestResource.java @@ -11,13 +11,18 @@ import io.quarkus.test.common.WithTestResource.List; /** - * Used to define a test resource. - * + * Used to define a test resource, which can affect various aspects of the application lifecycle. + *

+ * Note: When using the {@code restrictToAnnotatedClass=true} (which is the default), each test that is annotated + * with {@code @WithTestResource} will result in the application being re-augmented and restarted (in a similar fashion + * as happens in dev-mode when a change is detected) in order to incorporate the settings configured by the annotation. + * If there are many instances of the annotation used throughout the testsuite, this could result in slow test execution. + *

* All {@code WithTestResource} annotations in the test module * are discovered (regardless of the test which contains the annotation) * and their corresponding {@code QuarkusTestResourceLifecycleManager} * started before any test is run. - * + *

* Note that test resources are never restarted when running {@code @Nested} test classes. *

* This replaces {@link QuarkusTestResource}. The only difference is that the default value for @@ -55,6 +60,10 @@ * Whether this annotation should only be enabled if it is placed on the currently running test class or test profile. * Note that this defaults to true for meta-annotations since meta-annotations are only considered * for the current test class or test profile. + *

+ * Note: When this is set to {@code true} (which is the default), the annotation {@code @WithTestResource} will result + * in the application being re-augmented and restarted (in a similar fashion as happens in dev-mode when a change is + * detected) in order to incorporate the settings configured by the annotation. */ boolean restrictToAnnotatedClass() default true; From 54ee0651bf8bda8548013a539c1763beeaf69e0a Mon Sep 17 00:00:00 2001 From: xstefank Date: Fri, 9 Aug 2024 07:51:39 +0200 Subject: [PATCH 04/22] Fix methos to methods typos (cherry picked from commit 58560552f6218185cf675de9a158dadaaf0a1861) --- docs/src/main/asciidoc/websockets-next-reference.adoc | 4 ++-- .../qute/core/src/main/java/io/quarkus/qute/TemplateData.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc index 7d35092c93761..b37e40f180791 100644 --- a/docs/src/main/asciidoc/websockets-next-reference.adoc +++ b/docs/src/main/asciidoc/websockets-next-reference.adoc @@ -593,7 +593,7 @@ Methods annotated with `@OnOpen`, `@OnTextMessage`, `@OnBinaryMessage`, and `@On @Inject WebSocketConnection connection; ---- -NOTE: Note that outside of these methos, the `WebSocketConnection` object is not available. However, it is possible to <>. +NOTE: Note that outside of these methods, the `WebSocketConnection` object is not available. However, it is possible to <>. The connection can be used to send messages to the client, access the path parameters, broadcast messages to all connected clients, etc. @@ -909,7 +909,7 @@ Methods annotated with `@OnOpen`, `@OnTextMessage`, `@OnBinaryMessage`, and `@On @Inject WebSocketClientConnection connection; ---- -NOTE: Note that outside of these methos, the `WebSocketClientConnection` object is not available. However, it is possible to <>. +NOTE: Note that outside of these methods, the `WebSocketClientConnection` object is not available. However, it is possible to <>. The connection can be used to send messages to the client, access the path parameters, etc. diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateData.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateData.java index 6f0542d3a0820..847c4315110b4 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateData.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateData.java @@ -18,7 +18,7 @@ * non-public members, constructors, static initializers, static, synthetic and void methods are always ignored. *

* If the {@link #namespace()} is set to a non-empty value then a namespace resolver is automatically generated to access static - * fields and methos of the target class. + * fields and methods of the target class. * * @see ValueResolver * @see NamespaceResolver @@ -60,7 +60,7 @@ boolean properties() default false; /** - * If set to a non-empty value then a namespace resolver is automatically generated to access static fields and methos of + * If set to a non-empty value then a namespace resolver is automatically generated to access static fields and methods of * the target class. *

* By default, the namespace is the FQCN of the target class where dots and dollar signs are replaced by underscores, for From 5f85bef68ae17687b192a0091b7778bcf651ba18 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Thu, 8 Aug 2024 23:32:26 -0300 Subject: [PATCH 05/22] Fixes potential NPE when custom converters receive empty parameters - Fixes #40287 Co-Authored-By: Michel do Couto (cherry picked from commit 56d40f1884bc0d84924bd15df19c808e3a55cae0) --- .../converters/RuntimeParamConverterTest.java | 142 ++++++++++++++++++ .../converters/RuntimeResolvedConverter.java | 2 +- 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/converters/RuntimeParamConverterTest.java diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/converters/RuntimeParamConverterTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/converters/RuntimeParamConverterTest.java new file mode 100644 index 0000000000000..6c299ad6a6cfe --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/converters/RuntimeParamConverterTest.java @@ -0,0 +1,142 @@ +package io.quarkus.resteasy.reactive.server.test.converters; + +import static io.restassured.RestAssured.given; + +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Optional; +import java.util.function.Supplier; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ParamConverter; +import jakarta.ws.rs.ext.ParamConverterProvider; +import jakarta.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +class RuntimeParamConverterTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(ParamConverterEndpoint.class, OptionalIntegerParamConverterProvider.class, + OptionalIntegerParamConverter.class); + } + }); + + @Test + void sendParameters() { + given().queryParam("number", 22) + .when().get("/param-converter") + .then() + .statusCode(200) + .body(Matchers.is("Hello, 22!")); + } + + @Test + void doNotSendParameters() { + given().when().get("/param-converter") + .then() + .statusCode(200) + .body(Matchers.is("Hello, world! No number was provided.")); + } + + @Test + void sendEmptyParameter() { + given().queryParam("number", "") + .when().get("/param-converter") + .then() + .statusCode(200) + .body(Matchers.is("Hello! You provided an empty number.")); + } + + @ApplicationScoped + @Path("/param-converter") + public static class ParamConverterEndpoint { + + @GET + public Response greet(@QueryParam("number") Optional numberOpt) { + if (numberOpt != null) { + if (numberOpt.isPresent()) { + return Response.ok(String.format("Hello, %s!", numberOpt.get())).build(); + } else { + return Response.ok("Hello! You provided an empty number.").build(); + } + } else { + return Response.ok("Hello, world! No number was provided.").build(); + } + } + } + + @Provider + @ApplicationScoped + public static class OptionalIntegerParamConverterProvider implements ParamConverterProvider { + + @SuppressWarnings("unchecked") + @Override + public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { + if (rawType.equals(Optional.class)) { + if (genericType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericType; + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + if (typeArguments.length == 1 && typeArguments[0].equals(Integer.class)) { + return (ParamConverter) new OptionalIntegerParamConverter(); + } + } + } + + return null; + } + } + + public static class OptionalIntegerParamConverter implements ParamConverter> { + + @Override + public Optional fromString(String value) { + if (value == null) { + return null; + } + + if (value.trim().isEmpty()) { + return Optional.empty(); + } + + try { + int parsedInt = Integer.parseInt(value); + return Optional.of(parsedInt); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid integer value"); + } + } + + @Override + public String toString(Optional value) { + if (!value.isPresent()) { + return null; + } + + Integer intValue = value.get(); + if (intValue == null) { + return null; + } else { + return intValue.toString(); + } + } + + } + +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/RuntimeResolvedConverter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/RuntimeResolvedConverter.java index 84f9e793affce..983e4fafb0ac1 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/RuntimeResolvedConverter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/RuntimeResolvedConverter.java @@ -21,7 +21,7 @@ public RuntimeResolvedConverter(ParameterConverter quarkusConverter) { @Override public Object convert(Object parameter) { if (runtimeConverter != null) - return runtimeConverter.fromString(parameter.toString()); + return runtimeConverter.fromString(parameter != null ? parameter.toString() : null); return quarkusConverter.convert(parameter); } From f308efff1523e4b21f8153adff103797359138d8 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Sat, 10 Aug 2024 15:52:26 +0100 Subject: [PATCH 06/22] Remove dead links (cherry picked from commit d83cc9a356ad2c4f236df4fa516ecca60f919d9a) --- docs/src/main/asciidoc/podman.adoc | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/src/main/asciidoc/podman.adoc b/docs/src/main/asciidoc/podman.adoc index 05a21f8faeb26..411b6b0586870 100644 --- a/docs/src/main/asciidoc/podman.adoc +++ b/docs/src/main/asciidoc/podman.adoc @@ -22,12 +22,7 @@ The Homebrew package manager on Mac (*brew*) *should not be used to install Podm On Linux, Podman is integrated as part of the operating system, and installed through the system's packager manager. As with Mac, and Windows, Podman Desktop can also be installed to supplement the Podman CLI. However, on Linux, Podman Desktop acts as a client to the native Podman integration, and does not manage the underlying Podman installation. -See https://podman-desktop.io/downloads/ for the latest version of Podman Desktop or pick the version that suits your operating system from the list below: - -- https://podman-desktop.io/macos/[MacOS] -- https://podman-desktop.io/windows/[Windows] -- https://podman-desktop.io/linux/[Linux] - +See https://podman-desktop.io/downloads/ for the latest version of Podman Desktop. Additionally, if you are using Linux, see the Podman https://podman.io/docs/installation#installing-on-linux[Linux installation documentation] for instructions installing Podman to your specific Linux distribution. From e0f8bba70b256d0b21abfe3b6583c9918021bb2d Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Sun, 11 Aug 2024 20:31:39 +0200 Subject: [PATCH 07/22] Fix connection port for MongoDB dev services using shared network The hostname was fixed in #42065 but when using a shared network and a container build, the application will connect directly to the container port so we shouldn't use the exposed port but the original one. Fixes #42453 (cherry picked from commit e853d39924524c8029cf609a263a0772010e1c39) --- .../deployment/DevServicesMongoProcessor.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java index c8506b9511233..eb78f9a02d22d 100644 --- a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java @@ -178,7 +178,7 @@ private RunningDevService startMongo(DockerStatusBuildItem dockerStatusBuildItem } Supplier defaultMongoServerSupplier = () -> { - MongoDBContainer mongoDBContainer; + QuarkusMongoDBContainer mongoDBContainer; if (capturedProperties.imageName != null) { mongoDBContainer = new QuarkusMongoDBContainer( DockerImageName.parse(capturedProperties.imageName).asCompatibleSubstituteFor("mongo"), @@ -189,8 +189,9 @@ private RunningDevService startMongo(DockerStatusBuildItem dockerStatusBuildItem timeout.ifPresent(mongoDBContainer::withStartupTimeout); mongoDBContainer.withEnv(capturedProperties.containerEnv); mongoDBContainer.start(); - final String effectiveUrl = getEffectiveUrl(configPrefix, mongoDBContainer.getHost(), - mongoDBContainer.getMappedPort(MONGO_EXPOSED_PORT), capturedProperties); + + final String effectiveUrl = getEffectiveUrl(configPrefix, mongoDBContainer.getEffectiveHost(), + mongoDBContainer.getEffectivePort(), capturedProperties); return new RunningDevService(Feature.MONGODB_CLIENT.getName(), mongoDBContainer.getContainerId(), mongoDBContainer::close, getConfigPrefix(connectionName) + "connection-string", effectiveUrl); }; @@ -314,9 +315,12 @@ public String getReplicaSetUrl(String databaseName) { } } - @Override - public String getHost() { + public String getEffectiveHost() { return useSharedNetwork ? hostName : super.getHost(); } + + public Integer getEffectivePort() { + return useSharedNetwork ? MONGODB_INTERNAL_PORT : getMappedPort(MONGO_EXPOSED_PORT); + } } } From 8d04a721842cf2920ecea379f6928897d6ed02b5 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Mon, 12 Aug 2024 11:13:39 +0200 Subject: [PATCH 08/22] Switch back to Detached configuration for conditional dependencies in Gradle (cherry picked from commit 1e1e6e5f28c6e4e947f109edc73fd2666134f361) --- .../ConditionalDependenciesEnabler.java | 47 ++++--------------- 1 file changed, 9 insertions(+), 38 deletions(-) diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java index 4c0bc2fdee5ab..523ab4d735cd7 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java @@ -168,44 +168,15 @@ private void queueConditionalDependency(ExtensionDependency extension, Depend } private Configuration createConditionalDependenciesConfiguration(Project project, Dependency conditionalDep) { - // previously we used a detached configuration here but apparently extendsFrom(enforcedPlatforms) - // wouldn't actually enforce platforms on a detached configuration - var name = getConditionalConfigurationName(conditionalDep); - var config = project.getConfigurations().findByName(name); - if (config == null) { - project.getConfigurations().register(name, configuration -> { - configuration.setCanBeConsumed(false); - configuration.extendsFrom(enforcedPlatforms); - configuration.getDependencies().add(conditionalDep); - }); - config = project.getConfigurations().getByName(name); - } - return config; - } - - private static String getConditionalConfigurationName(Dependency conditionalDep) { - var name = new StringBuilder().append("quarkusConditional"); - appendCapitalized(name, conditionalDep.getGroup()); - appendCapitalized(name, conditionalDep.getName()); - appendCapitalized(name, conditionalDep.getVersion()); - return name.append("Configuration").toString(); - } - - private static void appendCapitalized(StringBuilder sb, String part) { - if (part != null && !part.isEmpty()) { - boolean toUpperCase = true; - for (int i = 0; i < part.length(); ++i) { - var c = part.charAt(i); - if (toUpperCase) { - sb.append(Character.toUpperCase(c)); - toUpperCase = false; - } else if (c == '.' || c == '-') { - toUpperCase = true; - } else { - sb.append(c); - } - } - } + // Ideally, we want a named configuration here to make sure platform version constraints are enforced + // on conditional dependencies (https://github.com/gradle/gradle/issues/6881) + // However, if we use a named configuration we run into issues preventing IDEs to import projects + // (https://github.com/quarkusio/quarkus/issues/41825) and ./gradlew refreshVersions seems to break as well. + Configuration conditionalDepConfiguration = project.getConfigurations() + .detachedConfiguration() + .extendsFrom(enforcedPlatforms); + conditionalDepConfiguration.getDependencies().add(conditionalDep); + return conditionalDepConfiguration; } private void enableConditionalDependency(ModuleVersionIdentifier dependency) { From 8c9b9d856a6889e3671ac9024e890f2a6b295c19 Mon Sep 17 00:00:00 2001 From: Thomas Canava Date: Mon, 12 Aug 2024 15:46:16 +0200 Subject: [PATCH 09/22] Make the boot jars in jib respect current timestamp (cherry picked from commit b401c5a3406eaa11b62d02d49c48b8f88a20c2b2) --- .../image/jib/deployment/JibProcessor.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index 38d9bf90817ad..b1856a0077c48 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -540,15 +540,20 @@ private JibContainerBuilder createContainerBuilderFromFastJar(String baseJvmImag FileEntriesLayer.Builder bootLibsLayerBuilder = FileEntriesLayer.builder().setName("fast-jar-boot-libs"); Path bootLibPath = componentsPath.resolve(JarResultBuildStep.LIB).resolve(JarResultBuildStep.BOOT_LIB); - try (Stream boolLibPaths = Files.list(bootLibPath)) { - boolLibPaths.forEach(lib -> { + try (Stream bootLibPaths = Files.list(bootLibPath)) { + bootLibPaths.forEach(lib -> { try { AbsoluteUnixPath libPathInContainer = workDirInContainer.resolve(JarResultBuildStep.LIB) .resolve(JarResultBuildStep.BOOT_LIB) .resolve(lib.getFileName()); - // the boot lib jars need to preserve the modification time because otherwise AppCDS won't work - bootLibsLayerBuilder.addEntry(lib, libPathInContainer, - Files.getLastModifiedTime(lib).toInstant()); + Instant bootLibModificationTime; + if (appCDSResult.isPresent()) { + // the boot lib jars need to preserve the modification time because otherwise AppCDS won't work + bootLibModificationTime = Files.getLastModifiedTime(lib).toInstant(); + } else { + bootLibModificationTime = modificationTime; + } + bootLibsLayerBuilder.addEntry(lib, libPathInContainer, bootLibModificationTime); } catch (IOException e) { throw new UncheckedIOException(e); } From 1d6462aff708f4b4b24b797626cb3e209a6ecf5d Mon Sep 17 00:00:00 2001 From: Rolfe Dlugy-Hegwer Date: Tue, 13 Aug 2024 18:38:09 -0400 Subject: [PATCH 10/22] Remove quarkus-panache-common from docs (cherry picked from commit 56c712d15242e8bfda2198d6622c58a911ac7be3) --- docs/src/main/asciidoc/mongodb-panache.adoc | 31 --------------------- 1 file changed, 31 deletions(-) diff --git a/docs/src/main/asciidoc/mongodb-panache.adoc b/docs/src/main/asciidoc/mongodb-panache.adoc index 35c583a5d3354..60ab35867ab0e 100644 --- a/docs/src/main/asciidoc/mongodb-panache.adoc +++ b/docs/src/main/asciidoc/mongodb-panache.adoc @@ -90,37 +90,6 @@ If you don't want to generate a new project, add the dependency in your build fi implementation("io.quarkus:quarkus-mongodb-panache") ---- -[NOTE] -==== -If your project is already configured to use other annotation processors, you will need to additionally add the Panache annotation processor: - -[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] -.pom.xml ----- - - maven-compiler-plugin - ${compiler-plugin.version} - - ${maven.compiler.parameters} - - - - io.quarkus - quarkus-panache-common - ${quarkus.platform.version} - - - - ----- - -[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] -.build.gradle ----- -annotationProcessor("io.quarkus:quarkus-panache-common") ----- -==== - == Setting up and configuring MongoDB with Panache To get started: From 8ecf4342db72047f49a2ad818d4e0b63a145a259 Mon Sep 17 00:00:00 2001 From: Rolfe Dlugy-Hegwer Date: Tue, 13 Aug 2024 17:53:01 -0400 Subject: [PATCH 11/22] Updates to reflect current extension names and configuration roots (cherry picked from commit 9f553398e33fbe8b116596a5e11283eec2b03920) --- .../asciidoc/security-csrf-prevention.adoc | 18 +++++++++--------- ...curity-openid-connect-client-reference.adoc | 17 ++++++++--------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/src/main/asciidoc/security-csrf-prevention.adoc b/docs/src/main/asciidoc/security-csrf-prevention.adoc index 45d3b372be5a2..154a53f0ea5e0 100644 --- a/docs/src/main/asciidoc/security-csrf-prevention.adoc +++ b/docs/src/main/asciidoc/security-csrf-prevention.adoc @@ -126,8 +126,8 @@ At this stage no additional configuration is needed - by default the CSRF form f [source,properties] ---- -quarkus.csrf-reactive.form-field-name=csrftoken -quarkus.csrf-reactive.cookie-name=csrftoken +quarkus.rest-csrf.form-field-name=csrftoken +quarkus.rest-csrf.cookie-name=csrftoken ---- == Sign CSRF token @@ -136,7 +136,7 @@ You can get `HMAC` signatures created for the generated CSRF tokens and have the [source,properties] ---- -quarkus.csrf-reactive.token-signature-key=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +quarkus.rest-csrf.token-signature-key=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow ---- [[csrf-request-header]] @@ -151,18 +151,18 @@ If HTML `form` tags are not used and you need to pass CSRF token as a header, th ---- <1> This expression is used to inject a CSRF token header and token. This token will be verified by the CSRF filter against a CSRF cookie. -Default header name is `X-CSRF-TOKEN`, you can customize it with `quarkus.csrf-reactive.token-header-name`, for example: +Default header name is `X-CSRF-TOKEN`, you can customize it with `quarkus.rest-csrf.token-header-name`, for example: [source,properties] ---- -quarkus.csrf-reactive.token-header-name=CUSTOM-X-CSRF-TOKEN +quarkus.rest-csrf.token-header-name=CUSTOM-X-CSRF-TOKEN ---- If you need to access the CSRF cookie from JavaScript in order to pass its value as a header, use `{inject:csrf.cookieName}` and `{inject:csrf.headerName}` to inject the cookie name which has to be read as a CSRF header value and allow accessing this cookie: [source,properties] ---- -quarkus.csrf-reactive.cookie-http-only=false +quarkus.rest-csrf.cookie-http-only=false ---- == Cross-origin resource sharing @@ -255,11 +255,11 @@ As you can see a CSRF token verification will be required at the `/service/user` [source,properties] ---- # Verify CSRF token only for the `/service/user` path, ignore other paths such as `/service/users` -quarkus.csrf-reactive.create-token-path=/service/user +quarkus.rest-csrf.create-token-path=/service/user # If `/service/user` path accepts not only `application/x-www-form-urlencoded` payloads but also other ones such as JSON then allow them # Setting this property is not necessary when the token is submitted as a header value -quarkus.csrf-reactive.require-form-url-encoded=false +quarkus.rest-csrf.require-form-url-encoded=false ---- == Verify CSRF token in the application code @@ -316,7 +316,7 @@ Also disable the token verification in the filter: [source,properties] ---- -quarkus.csrf-reactive.verify-token=false +quarkus.rest-csrf.verify-token=false ---- [[csrf-reactive-configuration-reference]] diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index 17cc43c346a70..5f5662159820c 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -1160,7 +1160,7 @@ quarkus.oidc-client.credentials.secret=secret quarkus.oidc-client.grant.type=exchange quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange -quarkus.oidc-token-propagation.exchange-token=true <1> +quarkus.resteasy-client-oidc-token-propagation.exchange-token=true <1> ---- <1> Please note that the `exchange-token` configuration property is ignored when the OidcClient name is set with the `io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient` annotation attribute. @@ -1178,11 +1178,10 @@ quarkus.oidc-client.grant.type=jwt quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access -quarkus.oidc-token-propagation.exchange-token=true +quarkus.resteasy-client-oidc-token-propagation.exchange-token=true ---- -`AccessTokenRequestReactiveFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-token-propagation-reactive.client-name` configuration property or with the `io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient` annotation attribute. -endif::no-quarkus-oidc-token-propagation-reactive[] +`AccessTokenRequestReactiveFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.rest-client-oidc-token-propagation.client-name` configuration property or with the `io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient` annotation attribute. ifndef::no-quarkus-oidc-token-propagation[] [[token-propagation]] @@ -1239,7 +1238,7 @@ public interface ProtectedResourceService { } ---- -Alternatively, `AccessTokenRequestFilter` can be registered automatically with all MP Rest or Jakarta REST clients if the `quarkus.oidc-token-propagation.register-filter` property is set to `true` and `quarkus.oidc-token-propagation.json-web-token` property is set to `false` (which is a default value). +Alternatively, `AccessTokenRequestFilter` can be registered automatically with all MP Rest or Jakarta REST clients if the `quarkus.resteasy-client-oidc-token-propagation.register-filter` property is set to `true` and `quarkus.resteasy-client-oidc-token-propagation.json-web-token` property is set to `false` (which is a default value). ==== Exchange token before propagation @@ -1253,7 +1252,7 @@ quarkus.oidc-client.credentials.secret=secret quarkus.oidc-client.grant.type=exchange quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange -quarkus.oidc-token-propagation.exchange-token=true +quarkus.resteasy-client-oidc-token-propagation.exchange-token=true ---- If you work with providers such as `Azure` that link:https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#example[require using] link:https://www.rfc-editor.org/rfc/rfc7523#section-2.1[JWT bearer token grant] to exchange the current token, then you can configure `AccessTokenRequestFilter` to exchange the token like this: @@ -1268,12 +1267,12 @@ quarkus.oidc-client.grant.type=jwt quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access -quarkus.oidc-token-propagation.exchange-token=true +quarkus.resteasy-client-oidc-token-propagation.exchange-token=true ---- Note `AccessTokenRequestFilter` will use `OidcClient` to exchange the current token, and you can use `quarkus.oidc-client.grant-options.exchange` to set the additional exchange properties expected by your OpenID Connect Provider. -`AccessTokenRequestFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-token-propagation.client-name` configuration property. +`AccessTokenRequestFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.resteasy-client-oidc-token-propagation.client-name` configuration property. === RestClient JsonWebTokenRequestFilter @@ -1315,7 +1314,7 @@ public interface ProtectedResourceService { } ---- -Alternatively, `JsonWebTokenRequestFilter` can be registered automatically with all MicroProfile REST or Jakarta REST clients if both `quarkus.oidc-token-propagation.register-filter` and `quarkus.resteasy-client-oidc-token-propagation.json-web-token` properties are set to `true`. +Alternatively, `JsonWebTokenRequestFilter` can be registered automatically with all MicroProfile REST or Jakarta REST clients if both `quarkus.resteasy-client-oidc-token-propagation.register-filter` and `quarkus.resteasy-client-oidc-token-propagation.json-web-token` properties are set to `true`. ==== Update token before propagation From 11119ac0f8e6d1dbbe1177edc06edfeb5b9eafb3 Mon Sep 17 00:00:00 2001 From: frne Date: Mon, 12 Aug 2024 11:29:07 +0200 Subject: [PATCH 12/22] Fix SmallRye Health OpenAPI definitions The quarkus-smallrye-health extension exposes an openapi definition for the health endpoints it provides. This fixes the exposed schema to actually match the JSON structure returned by the endpoints. Additionally, the filter implementation (io.quarkus.smallrye.health.deployment.HealthOpenAPIFilter) has been refactored to use a more fluid and readable schema definition. (cherry picked from commit 6394b665f0a42a0dac03c789c1d0a7a774825e0b) --- .../deployment/HealthOpenAPIFilter.java | 291 +++++++----------- .../health/test/HealthOpenAPITest.java | 11 +- 2 files changed, 125 insertions(+), 177 deletions(-) diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java index feb37ea27dc5f..62d7fd5232743 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java @@ -1,8 +1,6 @@ package io.quarkus.smallrye.health.deployment; -import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -12,9 +10,7 @@ import org.eclipse.microprofile.openapi.models.PathItem; import org.eclipse.microprofile.openapi.models.Paths; import org.eclipse.microprofile.openapi.models.media.Content; -import org.eclipse.microprofile.openapi.models.media.MediaType; import org.eclipse.microprofile.openapi.models.media.Schema; -import org.eclipse.microprofile.openapi.models.responses.APIResponse; import org.eclipse.microprofile.openapi.models.responses.APIResponses; import io.smallrye.openapi.api.models.ComponentsImpl; @@ -31,9 +27,42 @@ * Create OpenAPI entries (if configured) */ public class HealthOpenAPIFilter implements OASFilter { + private static final List MICROPROFILE_HEALTH_TAG = Collections.singletonList("MicroProfile Health"); - private static final String SCHEMA_HEALTH_RESPONSE = "HealthCheckResponse"; - private static final String SCHEMA_HEALTH_STATUS = "HealthCheckStatus"; + private static final String HEALTH_RESPONSE_SCHEMA_NAME = "HealthResponse"; + private static final String HEALTH_CHECK_SCHEMA_NAME = "HealthCheck"; + + private static final Schema healthResponseSchemaDefinition = new SchemaImpl(HEALTH_RESPONSE_SCHEMA_NAME) + .type(Schema.SchemaType.OBJECT) + .properties(Map.ofEntries( + + Map.entry("status", + new SchemaImpl() + .type(Schema.SchemaType.STRING) + .enumeration(List.of("UP", "DOWN"))), + + Map.entry("checks", + new SchemaImpl() + .type(Schema.SchemaType.ARRAY) + .items(new SchemaImpl().ref("#/components/schemas/" + HEALTH_CHECK_SCHEMA_NAME))))); + + private static final Schema healthCheckSchemaDefinition = new SchemaImpl(HEALTH_CHECK_SCHEMA_NAME) + .type(Schema.SchemaType.OBJECT) + .properties(Map.ofEntries( + + Map.entry("name", + new SchemaImpl() + .type(Schema.SchemaType.STRING)), + + Map.entry("status", + new SchemaImpl() + .type(Schema.SchemaType.STRING) + .enumeration(List.of("UP", "DOWN"))), + + Map.entry("data", + new SchemaImpl() + .type(Schema.SchemaType.OBJECT) + .nullable(Boolean.TRUE)))); private final String rootPath; private final String livenessPath; @@ -52,192 +81,102 @@ public void filterOpenAPI(OpenAPI openAPI) { if (openAPI.getComponents() == null) { openAPI.setComponents(new ComponentsImpl()); } - openAPI.getComponents().addSchema(SCHEMA_HEALTH_RESPONSE, createHealthCheckResponse()); - openAPI.getComponents().addSchema(SCHEMA_HEALTH_STATUS, createHealthCheckStatus()); + openAPI.getComponents().addSchema(HEALTH_RESPONSE_SCHEMA_NAME, healthResponseSchemaDefinition); + openAPI.getComponents().addSchema(HEALTH_CHECK_SCHEMA_NAME, healthCheckSchemaDefinition); if (openAPI.getPaths() == null) { openAPI.setPaths(new PathsImpl()); } - Paths paths = openAPI.getPaths(); + + final Paths paths = openAPI.getPaths(); // Health - paths.addPathItem(rootPath, createHealthPathItem()); + paths.addPathItem( + rootPath, + createHealthEndpoint( + "MicroProfile Health Endpoint", + "MicroProfile Health provides a way for your application to distribute " + + "information about its healthiness state to state whether or not it is able to " + + "function properly", + "Check the health of the application", + "microprofile_health_root", + "An aggregated view of the Liveness, Readiness and Startup of this application")); // Liveness - paths.addPathItem(livenessPath, createLivenessPathItem()); + paths.addPathItem( + livenessPath, + createHealthEndpoint( + "MicroProfile Health - Liveness Endpoint", + "Liveness checks are utilized to tell whether the application should be " + + "restarted", + "Check the liveness of the application", + "microprofile_health_liveness", + "The Liveness check of this application")); // Readiness - paths.addPathItem(readinessPath, createReadinessPathItem()); + paths.addPathItem( + readinessPath, + createHealthEndpoint( + "MicroProfile Health - Readiness Endpoint", + "Readiness checks are used to tell whether the application is able to " + + "process requests", + "Check the readiness of the application", + "microprofile_health_readiness", + "The Readiness check of this application")); // Startup - paths.addPathItem(startupPath, createStartupPathItem()); - } - - private PathItem createHealthPathItem() { - PathItem pathItem = new PathItemImpl(); - pathItem.setDescription("MicroProfile Health Endpoint"); - pathItem.setSummary( - "MicroProfile Health provides a way for your application to distribute information about its healthiness state to state whether or not it is able to function properly"); - pathItem.setGET(createHealthOperation()); - return pathItem; - } - - private PathItem createLivenessPathItem() { - PathItem pathItem = new PathItemImpl(); - pathItem.setDescription("MicroProfile Health - Liveness Endpoint"); - pathItem.setSummary( - "Liveness checks are utilized to tell whether the application should be restarted"); - pathItem.setGET(createLivenessOperation()); - return pathItem; - } - - private PathItem createReadinessPathItem() { - PathItem pathItem = new PathItemImpl(); - pathItem.setDescription("MicroProfile Health - Readiness Endpoint"); - pathItem.setSummary( - "Readiness checks are used to tell whether the application is able to process requests"); - pathItem.setGET(createReadinessOperation()); - return pathItem; - } - - private PathItem createStartupPathItem() { - PathItem pathItem = new PathItemImpl(); - pathItem.setDescription("MicroProfile Health - Startup Endpoint"); - pathItem.setSummary( - "Startup checks are an used to tell when the application has started"); - pathItem.setGET(createStartupOperation()); - return pathItem; - } - - private Operation createHealthOperation() { - Operation operation = new OperationImpl(); - operation.setDescription("Check the health of the application"); - operation.setOperationId("microprofile_health_root"); - operation.setTags(MICROPROFILE_HEALTH_TAG); - operation.setSummary("An aggregated view of the Liveness, Readiness and Startup of this application"); - operation.setResponses(createAPIResponses()); - return operation; - } - - private Operation createLivenessOperation() { - Operation operation = new OperationImpl(); - operation.setDescription("Check the liveness of the application"); - operation.setOperationId("microprofile_health_liveness"); - operation.setTags(MICROPROFILE_HEALTH_TAG); - operation.setSummary("The Liveness check of this application"); - operation.setResponses(createAPIResponses()); - return operation; - } - - private Operation createReadinessOperation() { - Operation operation = new OperationImpl(); - operation.setDescription("Check the readiness of the application"); - operation.setOperationId("microprofile_health_readiness"); - operation.setTags(MICROPROFILE_HEALTH_TAG); - operation.setSummary("The Readiness check of this application"); - operation.setResponses(createAPIResponses()); - return operation; - } - - private Operation createStartupOperation() { - Operation operation = new OperationImpl(); - operation.setDescription("Check the startup of the application"); - operation.setOperationId("microprofile_health_startup"); - operation.setTags(MICROPROFILE_HEALTH_TAG); - operation.setSummary("The Startup check of this application"); - operation.setResponses(createAPIResponses()); - return operation; - } - - private APIResponses createAPIResponses() { - APIResponses responses = new APIResponsesImpl(); - responses.addAPIResponse("200", createAPIResponse("OK")); - responses.addAPIResponse("503", createAPIResponse("Service Unavailable")); - responses.addAPIResponse("500", createAPIResponse("Internal Server Error")); - return responses; - } - - private APIResponse createAPIResponse(String description) { - APIResponse response = new APIResponseImpl(); - response.setDescription(description); - response.setContent(createContent()); - return response; - } - - private Content createContent() { - Content content = new ContentImpl(); - content.addMediaType("application/json", createMediaType()); - return content; - } - - private MediaType createMediaType() { - MediaType mediaType = new MediaTypeImpl(); - mediaType.setSchema(new SchemaImpl().ref("#/components/schemas/" + SCHEMA_HEALTH_RESPONSE)); - return mediaType; + paths.addPathItem( + startupPath, + createHealthEndpoint( + "MicroProfile Health - Startup Endpoint", + "Startup checks are an used to tell when the application has started", + "Check the startup of the application", + "microprofile_health_startup", + "The Startup check of this application")); } /** - * HealthCheckResponse: - * type: object - * properties: - * data: - * type: object - * nullable: true - * name: - * type: string - * status: - * $ref: '#/components/schemas/HealthCheckStatus' + * Creates a {@link PathItem} containing the endpoint definition and GET {@link Operation} for health endpoints. * - * @return Schema representing HealthCheckResponse + * @param endpointDescription The description for the endpoint definition + * @param endpointSummary The summary for the endpoint definition + * @param operationDescription The description for the operation definition + * @param operationId The operation-id for the operation definition + * @param operationSummary The summary for the operation definition */ - private Schema createHealthCheckResponse() { - Schema schema = new SchemaImpl(SCHEMA_HEALTH_RESPONSE); - schema.setType(Schema.SchemaType.OBJECT); - schema.setProperties(createProperties()); - return schema; - } - - private Map createProperties() { - Map map = new HashMap<>(); - map.put("data", createData()); - map.put("name", createName()); - map.put("status", new SchemaImpl().ref("#/components/schemas/" + SCHEMA_HEALTH_STATUS)); - return map; - } - - private Schema createData() { - Schema schema = new SchemaImpl("data"); - schema.setType(Schema.SchemaType.OBJECT); - schema.setNullable(Boolean.TRUE); - return schema; - } - - private Schema createName() { - Schema schema = new SchemaImpl("name"); - schema.setType(Schema.SchemaType.STRING); - return schema; - } - - /** - * HealthCheckStatus: - * enum: - * - DOWN - * - UP - * type: string - * - * @return Schema representing Status - */ - private Schema createHealthCheckStatus() { - Schema schema = new SchemaImpl(SCHEMA_HEALTH_STATUS); - schema.setEnumeration(createStateEnumValues()); - schema.setType(Schema.SchemaType.STRING); - return schema; - } - - private List createStateEnumValues() { - List values = new ArrayList<>(); - values.add("DOWN"); - values.add("UP"); - return values; + private PathItem createHealthEndpoint( + String endpointDescription, + String endpointSummary, + String operationDescription, + String operationId, + String operationSummary) { + final Content content = new ContentImpl() + .addMediaType( + "application/json", + new MediaTypeImpl() + .schema(new SchemaImpl().ref("#/components/schemas/" + HEALTH_RESPONSE_SCHEMA_NAME))); + + final APIResponses responses = new APIResponsesImpl() + .addAPIResponse( + "200", + new APIResponseImpl().description("OK").content(content)) + .addAPIResponse( + "503", + new APIResponseImpl().description("Service Unavailable").content(content)) + .addAPIResponse( + "500", + new APIResponseImpl().description("Internal Server Error").content(content)); + + final Operation getOperation = new OperationImpl() + .operationId(operationId) + .description(operationDescription) + .tags(MICROPROFILE_HEALTH_TAG) + .summary(operationSummary) + .responses(responses); + + return new PathItemImpl() + .description(endpointDescription) + .summary(endpointSummary) + .GET(getOperation); } } diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java index b0b33b1ccc189..c3c5fa4b4144b 100644 --- a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java @@ -32,7 +32,16 @@ void testOpenApiPathAccessResource() { .body("paths", Matchers.hasKey("/q/health/live")) .body("paths", Matchers.hasKey("/q/health/started")) .body("paths", Matchers.hasKey("/q/health")) - .body("components.schemas.HealthCheckResponse.type", Matchers.equalTo("object")); + + .body("components.schemas.HealthResponse.type", Matchers.equalTo("object")) + .body("components.schemas.HealthResponse.properties.status.type", Matchers.equalTo("string")) + .body("components.schemas.HealthResponse.properties.checks.type", Matchers.equalTo("array")) + + .body("components.schemas.HealthCheck.type", Matchers.equalTo("object")) + .body("components.schemas.HealthCheck.properties.status.type", Matchers.equalTo("string")) + .body("components.schemas.HealthCheck.properties.name.type", Matchers.equalTo("string")) + .body("components.schemas.HealthCheck.properties.data.type", Matchers.equalTo("object")) + .body("components.schemas.HealthCheck.properties.data.nullable", Matchers.is(true)); } From 976dd3d6e32337211bb077b2598a006ee5dd6fae Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 12 Aug 2024 18:07:37 +0200 Subject: [PATCH 13/22] Do not keep the whole Manifest in memory Some manifests are quite large, for instance the one for commons-codec is 21 KB. Given most of the information in the Manifest are of no interest to us, let's trim the Manifest to only the information we need. Also we still have the problem of us keeping the jars open multiple times in test and dev mode so it amplifies this issue. In some cases, I ended up with 1700+ jars opened so the Manifests add up quite quickly. (cherry picked from commit a1b3fa90fc920a1fd80ad0d1079f29625fa2226a) --- .../java/io/quarkus/paths/EmptyPathTree.java | 3 +- .../java/io/quarkus/paths/FilePathTree.java | 3 +- .../io/quarkus/paths/FilteredPathTree.java | 5 +- .../io/quarkus/paths/ManifestAttributes.java | 72 +++++++++++++++++++ .../io/quarkus/paths/MultiRootPathTree.java | 5 +- .../main/java/io/quarkus/paths/PathTree.java | 7 +- .../quarkus/paths/PathTreeWithManifest.java | 22 +++--- .../quarkus/paths/SharedArchivePathTree.java | 5 +- .../bootstrap/app/CuratedApplication.java | 6 +- .../AbstractClassPathElement.java | 21 +++--- .../classloading/ClassPathElement.java | 6 +- .../FilteredClassPathElement.java | 6 +- .../PathTreeClassPathElement.java | 6 +- .../classloading/QuarkusClassLoader.java | 21 +++--- 14 files changed, 127 insertions(+), 61 deletions(-) create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ManifestAttributes.java diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java index b57c35f57bf8a..578376dd258d0 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java @@ -6,7 +6,6 @@ import java.util.Collections; import java.util.function.Consumer; import java.util.function.Function; -import java.util.jar.Manifest; public class EmptyPathTree implements OpenPathTree { @@ -22,7 +21,7 @@ public Collection getRoots() { } @Override - public Manifest getManifest() { + public ManifestAttributes getManifestAttributes() { return null; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java index fcdbd6a9e21d0..b393007eec0fb 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java @@ -7,7 +7,6 @@ import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; -import java.util.jar.Manifest; class FilePathTree implements OpenPathTree { @@ -29,7 +28,7 @@ public Collection getRoots() { } @Override - public Manifest getManifest() { + public ManifestAttributes getManifestAttributes() { return null; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java index 8265ef59771f1..53c2a3cbbb1d5 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java @@ -6,7 +6,6 @@ import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; -import java.util.jar.Manifest; public class FilteredPathTree implements PathTree { @@ -24,8 +23,8 @@ public Collection getRoots() { } @Override - public Manifest getManifest() { - return original.getManifest(); + public ManifestAttributes getManifestAttributes() { + return original.getManifestAttributes(); } @Override diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ManifestAttributes.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ManifestAttributes.java new file mode 100644 index 0000000000000..34716ebb6b2e6 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ManifestAttributes.java @@ -0,0 +1,72 @@ +package io.quarkus.paths; + +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +/** + * Manifests for some libraries can be quite large (e.g. for commons-codec, it is 21 KB). + *

+ * Given we keep a lot of ArchivePathTree around, it seems like a good idea to only + * keep around the Manifest entries that we actually use in Quarkus. + *

+ * This can be extended further in the future if we need more attributes. + */ +public class ManifestAttributes { + + private String specificationTitle; + private String specificationVersion; + private String specificationVendor; + + private String implementationTitle; + private String implementationVersion; + private String implementationVendor; + + private boolean multiRelease; + + public static ManifestAttributes of(Manifest manifest) { + if (manifest == null) { + return null; + } + + return new ManifestAttributes(manifest); + } + + private ManifestAttributes(Manifest manifest) { + specificationTitle = manifest.getMainAttributes().getValue(Attributes.Name.SPECIFICATION_TITLE); + specificationVersion = manifest.getMainAttributes().getValue(Attributes.Name.SPECIFICATION_VERSION); + specificationVendor = manifest.getMainAttributes().getValue(Attributes.Name.SPECIFICATION_VENDOR); + implementationTitle = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_TITLE); + implementationVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION); + implementationVendor = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VENDOR); + + multiRelease = Boolean.parseBoolean(manifest.getMainAttributes().getValue(Attributes.Name.MULTI_RELEASE)); + } + + public String getSpecificationTitle() { + return specificationTitle; + } + + public String getSpecificationVersion() { + return specificationVersion; + } + + public String getSpecificationVendor() { + return specificationVendor; + } + + public String getImplementationTitle() { + return implementationTitle; + } + + public String getImplementationVersion() { + return implementationVersion; + } + + public String getImplementationVendor() { + return implementationVendor; + } + + public boolean isMultiRelease() { + return multiRelease; + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java index e09b70ce12168..5344d04854a2b 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java @@ -9,7 +9,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Function; -import java.util.jar.Manifest; public class MultiRootPathTree implements OpenPathTree { @@ -32,9 +31,9 @@ public Collection getRoots() { } @Override - public Manifest getManifest() { + public ManifestAttributes getManifestAttributes() { for (PathTree tree : trees) { - final Manifest m = tree.getManifest(); + final ManifestAttributes m = tree.getManifestAttributes(); if (m != null) { return m; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java index 5b68db900bc93..369f9d7610725 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java @@ -7,7 +7,6 @@ import java.util.Collection; import java.util.function.Consumer; import java.util.function.Function; -import java.util.jar.Manifest; public interface PathTree { @@ -98,12 +97,12 @@ default boolean isEmpty() { } /** - * If {@code META-INF/MANIFEST.MF} found, reads it and returns an instance of {@link java.util.jar.Manifest}, - * otherwise returns null. + * If {@code META-INF/MANIFEST.MF} found, reads it and returns an instance of {@link ManifestAttributes}, + * a trimmed down version of the Manifest, otherwise returns null. * * @return parsed {@code META-INF/MANIFEST.MF} if it's found, otherwise {@code null} */ - Manifest getManifest(); + ManifestAttributes getManifestAttributes(); /** * Walks the tree. diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTreeWithManifest.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTreeWithManifest.java index 8576f4303a30c..3b3608416f2d2 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTreeWithManifest.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTreeWithManifest.java @@ -42,7 +42,7 @@ public abstract class PathTreeWithManifest implements PathTree { protected boolean manifestEnabled; private final ReentrantReadWriteLock manifestInfoLock = new ReentrantReadWriteLock(); - private transient Manifest manifest; + private transient ManifestAttributes manifestAttributes; protected transient boolean manifestInitialized; protected volatile Map multiReleaseMapping; @@ -61,7 +61,7 @@ protected PathTreeWithManifest(boolean manifestEnabled) { protected PathTreeWithManifest(PathTreeWithManifest pathTreeWithManifest) { pathTreeWithManifest.manifestReadLock().lock(); try { - this.manifest = pathTreeWithManifest.manifest; + this.manifestAttributes = pathTreeWithManifest.manifestAttributes; this.manifestInitialized = pathTreeWithManifest.manifestInitialized; this.multiReleaseMapping = pathTreeWithManifest.multiReleaseMapping; } finally { @@ -78,12 +78,12 @@ public T apply(String relativePath, Function func) { protected abstract T apply(String relativePath, Function func, boolean manifestEnabled); @Override - public Manifest getManifest() { + public ManifestAttributes getManifestAttributes() { // Optimistically try with a lock that allows concurrent access first, for performance. manifestReadLock().lock(); try { if (manifestInitialized) { - return manifest; + return manifestAttributes; } } finally { manifestReadLock().unlock(); @@ -94,18 +94,18 @@ public Manifest getManifest() { if (manifestInitialized) { // Someone else got here between our call to manifestReadLock().unlock() // and our call to manifestWriteLock().lock(); it can happen. - return manifest; + return manifestAttributes; } final Manifest m = apply("META-INF/MANIFEST.MF", ManifestReader.INSTANCE, false); initManifest(m); } finally { manifestWriteLock().unlock(); } - return manifest; + return manifestAttributes; } protected void initManifest(Manifest m) { - manifest = m; + manifestAttributes = ManifestAttributes.of(m); manifestInitialized = true; } @@ -118,7 +118,9 @@ protected ReadLock manifestReadLock() { } public boolean isMultiReleaseJar() { - return isMultiReleaseJar(getManifest()); + ManifestAttributes manifestAttributes = getManifestAttributes(); + + return manifestAttributes != null && manifestAttributes.isMultiRelease(); } protected Map getMultiReleaseMapping() { @@ -144,10 +146,6 @@ protected String toMultiReleaseRelativePath(String relativePath) { return getMultiReleaseMapping().getOrDefault(relativePath, relativePath); } - private static boolean isMultiReleaseJar(final Manifest m) { - return m != null && Boolean.parseBoolean(m.getMainAttributes().getValue("Multi-Release")); - } - private static class MultiReleaseMappingReader implements Function> { private static final MultiReleaseMappingReader INSTANCE = new MultiReleaseMappingReader(); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java index edfef2d0c1d49..ef4871e7f4921 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java @@ -10,7 +10,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; -import java.util.jar.Manifest; /** * While {@link ArchivePathTree} implementation is thread-safe, this implementation @@ -144,8 +143,8 @@ public Collection getRoots() { } @Override - public Manifest getManifest() { - return delegate.getManifest(); + public ManifestAttributes getManifestAttributes() { + return delegate.getManifestAttributes(); } @Override diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java index 7b66d6c856413..89d54ad91fb0c 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java @@ -15,7 +15,6 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; -import java.util.jar.Manifest; import java.util.stream.Collectors; import io.quarkus.bootstrap.classloading.ClassPathElement; @@ -27,6 +26,7 @@ import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.DependencyFlags; import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.ManifestAttributes; import io.quarkus.paths.OpenPathTree; import io.quarkus.paths.PathTree; @@ -499,8 +499,8 @@ public ProtectionDomain getProtectionDomain() { } @Override - public Manifest getManifest() { - return delegate.getManifest(); + public ManifestAttributes getManifestAttributes() { + return delegate.getManifestAttributes(); } @Override diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/AbstractClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/AbstractClassPathElement.java index d2a2faed0a835..be85f0786cfd8 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/AbstractClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/AbstractClassPathElement.java @@ -2,37 +2,40 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.jar.Manifest; import org.jboss.logging.Logger; +import io.quarkus.paths.ManifestAttributes; + public abstract class AbstractClassPathElement implements ClassPathElement { private static final Logger log = Logger.getLogger(AbstractClassPathElement.class); - private volatile Manifest manifest; + private volatile ManifestAttributes manifestAttributes; private volatile boolean manifestInitialized = false; @Override - public Manifest getManifest() { + public ManifestAttributes getManifestAttributes() { if (manifestInitialized) { - return manifest; + return manifestAttributes; } synchronized (this) { if (manifestInitialized) { - return manifest; + return manifestAttributes; } - manifest = readManifest(); + manifestAttributes = readManifest(); manifestInitialized = true; } - return manifest; + return manifestAttributes; } - protected Manifest readManifest() { + protected ManifestAttributes readManifest() { final ClassPathResource mf = getResource("META-INF/MANIFEST.MF"); if (mf != null) { - try { - return new Manifest(new ByteArrayInputStream(mf.getData())); + try (InputStream manifestIs = new ByteArrayInputStream(mf.getData())) { + return ManifestAttributes.of(new Manifest(manifestIs)); } catch (IOException e) { log.warnf("Failed to parse manifest for %s", toString(), e); } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java index 0e319d437e7b7..96c6689a4816d 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java @@ -7,11 +7,11 @@ import java.util.List; import java.util.Set; import java.util.function.Function; -import java.util.jar.Manifest; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.paths.EmptyPathTree; +import io.quarkus.paths.ManifestAttributes; import io.quarkus.paths.OpenPathTree; import io.quarkus.paths.PathTree; @@ -80,7 +80,7 @@ default ResolvedDependency getResolvedDependency() { */ ProtectionDomain getProtectionDomain(); - Manifest getManifest(); + ManifestAttributes getManifestAttributes(); /** * Checks whether this is a runtime classpath element @@ -133,7 +133,7 @@ public ProtectionDomain getProtectionDomain() { } @Override - public Manifest getManifest() { + public ManifestAttributes getManifestAttributes() { return null; } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java index 3e6f9a897d277..a8dbbd2d06ec7 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java @@ -7,9 +7,9 @@ import java.util.HashSet; import java.util.Set; import java.util.function.Function; -import java.util.jar.Manifest; import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.ManifestAttributes; import io.quarkus.paths.OpenPathTree; public class FilteredClassPathElement implements ClassPathElement { @@ -63,9 +63,9 @@ public ProtectionDomain getProtectionDomain() { } @Override - public Manifest getManifest() { + public ManifestAttributes getManifestAttributes() { //we don't support filtering the manifest - return delegate.getManifest(); + return delegate.getManifestAttributes(); } @Override diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java index a1da4f0e74114..8126c264dbb43 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java @@ -21,11 +21,11 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; -import java.util.jar.Manifest; import org.jboss.logging.Logger; import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.ManifestAttributes; import io.quarkus.paths.OpenPathTree; import io.quarkus.paths.PathTree; import io.quarkus.paths.PathVisit; @@ -176,8 +176,8 @@ public void visitPath(PathVisit visit) { } @Override - protected Manifest readManifest() { - return apply(OpenPathTree::getManifest); + protected ManifestAttributes readManifest() { + return apply(OpenPathTree::getManifestAttributes); } @Override diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index 016dbdd191b44..14361e8d67a38 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -25,11 +25,11 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.jar.Attributes; -import java.util.jar.Manifest; import org.jboss.logging.Logger; +import io.quarkus.paths.ManifestAttributes; + /** * The ClassLoader used for non production Quarkus applications (i.e. dev and test mode). */ @@ -578,15 +578,14 @@ private void definePackage(String name, ClassPathElement classPathElement) { if ((pkgName != null) && definedPackages.get(pkgName) == null) { synchronized (getClassLoadingLock(pkgName)) { if (definedPackages.get(pkgName) == null) { - Manifest mf = classPathElement.getManifest(); - if (mf != null) { - Attributes ma = mf.getMainAttributes(); - definedPackages.put(pkgName, definePackage(pkgName, ma.getValue(Attributes.Name.SPECIFICATION_TITLE), - ma.getValue(Attributes.Name.SPECIFICATION_VERSION), - ma.getValue(Attributes.Name.SPECIFICATION_VENDOR), - ma.getValue(Attributes.Name.IMPLEMENTATION_TITLE), - ma.getValue(Attributes.Name.IMPLEMENTATION_VERSION), - ma.getValue(Attributes.Name.IMPLEMENTATION_VENDOR), null)); + ManifestAttributes manifest = classPathElement.getManifestAttributes(); + if (manifest != null) { + definedPackages.put(pkgName, definePackage(pkgName, manifest.getSpecificationTitle(), + manifest.getSpecificationVersion(), + manifest.getSpecificationVendor(), + manifest.getImplementationTitle(), + manifest.getImplementationVersion(), + manifest.getImplementationVendor(), null)); return; } From 8c949c6eca1feef1d3100fd06c0bf3492596b7c8 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 12 Aug 2024 21:46:13 +0200 Subject: [PATCH 14/22] Make sure we don't keep a reference to the ZipPath in ArchivePathTree A Path can actually contain references to the filesystem: this is the case of the ZipPath. We want to make sure we don't keep ZipFileSystem around when a path tree is closed so we also need to nullify the path. Finally, we make sure only DirectoryPathTree is marked as Serializable as we only serialize the workspace directories. (cherry picked from commit 6780c2c17788d59e30ff50238224416cd62786bb) --- .../io/quarkus/paths/ArchivePathTree.java | 54 +++--- .../io/quarkus/paths/CachingPathTree.java | 5 +- .../io/quarkus/paths/DirectoryPathTree.java | 142 ++------------ .../quarkus/paths/OpenContainerPathTree.java | 175 ++++++++++++++++++ .../main/java/io/quarkus/paths/PathTree.java | 3 + .../quarkus/paths/SharedArchivePathTree.java | 4 +- 6 files changed, 224 insertions(+), 159 deletions(-) create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/OpenContainerPathTree.java diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java index 507dce20e771a..d71c8b494b8e3 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java @@ -206,15 +206,30 @@ public boolean equals(Object obj) { && manifestEnabled == other.manifestEnabled; } - protected class OpenArchivePathTree extends DirectoryPathTree { + protected class OpenArchivePathTree extends OpenContainerPathTree { - // we don't make the field final as we want to nullify it on close - private volatile FileSystem fs; private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + // we don't make these fields final as we want to nullify them on close + private FileSystem fs; + private Path rootPath; + + private volatile boolean open = true; + protected OpenArchivePathTree(FileSystem fs) { - super(fs.getPath("/"), pathFilter, ArchivePathTree.this); + super(ArchivePathTree.this.pathFilter, ArchivePathTree.this); this.fs = fs; + this.rootPath = fs.getPath("/"); + } + + @Override + protected Path getContainerPath() { + return ArchivePathTree.this.archive; + } + + @Override + protected Path getRootPath() { + return rootPath; } protected ReentrantReadWriteLock.ReadLock readLock() { @@ -254,7 +269,7 @@ protected void initMultiReleaseMapping(Map mrMapping) { public boolean isOpen() { lock.readLock().lock(); try { - return fs != null && fs.isOpen(); + return open; } finally { lock.readLock().unlock(); } @@ -320,7 +335,7 @@ public Path getPath(String relativePath) { */ private void ensureOpen() { // let's not use isOpen() as ensureOpen() is always used inside a read lock - if (fs != null && fs.isOpen()) { + if (open) { return; } throw new RuntimeException("Failed to access " + ArchivePathTree.this.getRoots() @@ -329,28 +344,19 @@ private void ensureOpen() { @Override public void close() throws IOException { - Throwable t = null; lock.writeLock().lock(); try { - super.close(); - } catch (Throwable e) { - t = e; + open = false; + rootPath = null; + fs.close(); + } catch (IOException e) { throw e; } finally { - try { - fs.close(); - } catch (IOException e) { - if (t != null) { - e.addSuppressed(t); - } - throw e; - } finally { - // even when we close the fs, everything is kept as is in the fs instance - // and typically the cen, which is quite large - // let's make sure the fs is nullified for it to be garbage collected - fs = null; - lock.writeLock().unlock(); - } + // even when we close the fs, everything is kept as is in the fs instance + // and typically the cen, which is quite large + // let's make sure the fs is nullified for it to be garbage collected + fs = null; + lock.writeLock().unlock(); } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java index 47bde335702f4..d663d3a2bc9ef 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java @@ -9,7 +9,6 @@ import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; -import java.util.jar.Manifest; public class CachingPathTree implements OpenPathTree { @@ -30,8 +29,8 @@ public Collection getRoots() { } @Override - public Manifest getManifest() { - return delegate.getManifest(); + public ManifestAttributes getManifestAttributes() { + return delegate.getManifestAttributes(); } @Override diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java index 36a1c41e9cff7..afe5ee35962f4 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java @@ -2,55 +2,14 @@ import java.io.IOException; import java.io.Serializable; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collection; -import java.util.Collections; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.regex.Pattern; -public class DirectoryPathTree extends PathTreeWithManifest implements OpenPathTree, Serializable { +public class DirectoryPathTree extends OpenContainerPathTree implements Serializable { private static final long serialVersionUID = 2255956884896445059L; - private static final boolean USE_WINDOWS_ABSOLUTE_PATH_PATTERN = !FileSystems.getDefault().getSeparator().equals("/"); - - private static volatile Pattern windowsAbsolutePathPattern; - - private static Pattern windowsAbsolutePathPattern() { - return windowsAbsolutePathPattern == null ? windowsAbsolutePathPattern = Pattern.compile("[a-zA-Z]:\\\\.*") - : windowsAbsolutePathPattern; - } - - static boolean isAbsolutePath(String path) { - return path != null && !path.isEmpty() - && (path.charAt(0) == '/' // we want to check for '/' on every OS - || USE_WINDOWS_ABSOLUTE_PATH_PATTERN - && (windowsAbsolutePathPattern().matcher(path).matches()) - || path.startsWith(FileSystems.getDefault().getSeparator())); - } - - static void ensureResourcePath(FileSystem fs, String path) { - if (isAbsolutePath(path)) { - throw new IllegalArgumentException("Expected a path relative to the root of the path tree but got " + path); - } - // this is to disallow reading outside the path tree root - if (path != null && path.contains("..")) { - for (Path pathElement : fs.getPath(path)) { - if (pathElement.toString().equals("..")) { - throw new IllegalArgumentException("'..' cannot be used in resource paths, but got " + path); - } - } - } - } - private Path dir; - private PathFilter pathFilter; /** * For deserialization @@ -68,77 +27,37 @@ public DirectoryPathTree(Path dir, PathFilter pathFilter) { } public DirectoryPathTree(Path dir, PathFilter pathFilter, boolean manifestEnabled) { - super(manifestEnabled); + super(pathFilter, manifestEnabled); this.dir = dir; - this.pathFilter = pathFilter; } protected DirectoryPathTree(Path dir, PathFilter pathFilter, PathTreeWithManifest pathTreeWithManifest) { - super(pathTreeWithManifest); + super(pathFilter, pathTreeWithManifest); this.dir = dir; - this.pathFilter = pathFilter; } @Override - public Collection getRoots() { - return Collections.singletonList(dir); + protected Path getRootPath() { + return dir; } @Override - public void walk(PathVisitor visitor) { - PathTreeVisit.walk(dir, dir, pathFilter, getMultiReleaseMapping(), visitor); - } - - private void ensureResourcePath(String path) { - ensureResourcePath(dir.getFileSystem(), path); + protected Path getContainerPath() { + return dir; } @Override - protected T apply(String relativePath, Function func, boolean manifestEnabled) { - ensureResourcePath(relativePath); - if (!PathFilter.isVisible(pathFilter, relativePath)) { - return func.apply(null); - } - final Path path = dir.resolve(manifestEnabled ? toMultiReleaseRelativePath(relativePath) : relativePath); - if (!Files.exists(path)) { - return func.apply(null); - } - return PathTreeVisit.process(dir, dir, path, pathFilter, func); - } - - @Override - public void accept(String relativePath, Consumer consumer) { - ensureResourcePath(relativePath); - if (!PathFilter.isVisible(pathFilter, relativePath)) { - consumer.accept(null); - return; - } - final Path path = dir.resolve(manifestEnabled ? toMultiReleaseRelativePath(relativePath) : relativePath); - if (!Files.exists(path)) { - consumer.accept(null); - return; - } - PathTreeVisit.consume(dir, dir, path, pathFilter, consumer); + public boolean isOpen() { + return true; } @Override - public boolean contains(String relativePath) { - ensureResourcePath(relativePath); - if (!PathFilter.isVisible(pathFilter, relativePath)) { - return false; - } - final Path path = dir.resolve(manifestEnabled ? toMultiReleaseRelativePath(relativePath) : relativePath); - return Files.exists(path); + public void close() throws IOException { } @Override - public Path getPath(String relativePath) { - ensureResourcePath(relativePath); - if (!PathFilter.isVisible(pathFilter, relativePath)) { - return null; - } - final Path path = dir.resolve(manifestEnabled ? toMultiReleaseRelativePath(relativePath) : relativePath); - return Files.exists(path) ? path : null; + public PathTree getOriginalTree() { + return this; } private void writeObject(java.io.ObjectOutputStream out) throws IOException { @@ -153,41 +72,4 @@ private void readObject(java.io.ObjectInputStream in) throws IOException, ClassN manifestEnabled = in.readBoolean(); } - @Override - public OpenPathTree open() { - return this; - } - - @Override - public boolean isOpen() { - return true; - } - - @Override - public void close() throws IOException { - } - - @Override - public PathTree getOriginalTree() { - return this; - } - - @Override - public int hashCode() { - return Objects.hash(dir, pathFilter, manifestEnabled); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - DirectoryPathTree other = (DirectoryPathTree) obj; - return Objects.equals(dir, other.dir) && Objects.equals(pathFilter, other.pathFilter) - && manifestEnabled == other.manifestEnabled; - } - } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/OpenContainerPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/OpenContainerPathTree.java new file mode 100644 index 0000000000000..5def8099b50bc --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/OpenContainerPathTree.java @@ -0,0 +1,175 @@ +package io.quarkus.paths; + +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Pattern; + +public abstract class OpenContainerPathTree extends PathTreeWithManifest implements OpenPathTree { + + private static final boolean USE_WINDOWS_ABSOLUTE_PATH_PATTERN = !FileSystems.getDefault().getSeparator().equals("/"); + + private static volatile Pattern windowsAbsolutePathPattern; + + private static Pattern windowsAbsolutePathPattern() { + return windowsAbsolutePathPattern == null ? windowsAbsolutePathPattern = Pattern.compile("[a-zA-Z]:\\\\.*") + : windowsAbsolutePathPattern; + } + + static boolean isAbsolutePath(String path) { + return path != null && !path.isEmpty() + && (path.charAt(0) == '/' // we want to check for '/' on every OS + || USE_WINDOWS_ABSOLUTE_PATH_PATTERN + && (windowsAbsolutePathPattern().matcher(path).matches()) + || path.startsWith(FileSystems.getDefault().getSeparator())); + } + + static void ensureResourcePath(FileSystem fs, String path) { + if (isAbsolutePath(path)) { + throw new IllegalArgumentException("Expected a path relative to the root of the path tree but got " + path); + } + // this is to disallow reading outside the path tree root + if (path != null && path.contains("..")) { + for (Path pathElement : fs.getPath(path)) { + if (pathElement.toString().equals("..")) { + throw new IllegalArgumentException("'..' cannot be used in resource paths, but got " + path); + } + } + } + } + + protected PathFilter pathFilter; + + /** + * For deserialization of DirectoryPathTree + */ + public OpenContainerPathTree() { + } + + protected OpenContainerPathTree(PathFilter pathFilter) { + this(pathFilter, false); + } + + protected OpenContainerPathTree(PathFilter pathFilter, boolean manifestEnabled) { + super(manifestEnabled); + this.pathFilter = pathFilter; + } + + protected OpenContainerPathTree(PathFilter pathFilter, PathTreeWithManifest pathTreeWithManifest) { + super(pathTreeWithManifest); + this.pathFilter = pathFilter; + } + + /** + * This is the path to the container. + *

+ * In the case of a zip archive, it's the path of the archive. + * In the case of a directory, it's the directory. + *

+ * Should only be used for equals/hashCode. + */ + protected abstract Path getContainerPath(); + + /** + * This is the path to the container. + *

+ * In the case of a zip archive, it's the path to the root of the archive (i.e. a ZipPath). + * In the case of a directory, it's the directory. + *

+ * Should be used for any read operation on the container. + */ + protected abstract Path getRootPath(); + + @Override + public OpenPathTree open() { + return this; + } + + @Override + public Collection getRoots() { + return List.of(getContainerPath()); + } + + @Override + public void walk(PathVisitor visitor) { + PathTreeVisit.walk(getRootPath(), getRootPath(), pathFilter, getMultiReleaseMapping(), + visitor); + } + + private void ensureResourcePath(String path) { + ensureResourcePath(getRootPath().getFileSystem(), path); + } + + @Override + protected T apply(String relativePath, Function func, boolean manifestEnabled) { + ensureResourcePath(relativePath); + if (!PathFilter.isVisible(pathFilter, relativePath)) { + return func.apply(null); + } + final Path path = getRootPath().resolve(manifestEnabled ? toMultiReleaseRelativePath(relativePath) : relativePath); + if (!Files.exists(path)) { + return func.apply(null); + } + return PathTreeVisit.process(getRootPath(), getRootPath(), path, pathFilter, func); + } + + @Override + public void accept(String relativePath, Consumer consumer) { + ensureResourcePath(relativePath); + if (!PathFilter.isVisible(pathFilter, relativePath)) { + consumer.accept(null); + return; + } + final Path path = getRootPath().resolve(manifestEnabled ? toMultiReleaseRelativePath(relativePath) : relativePath); + if (!Files.exists(path)) { + consumer.accept(null); + return; + } + PathTreeVisit.consume(getRootPath(), getRootPath(), path, pathFilter, consumer); + } + + @Override + public boolean contains(String relativePath) { + ensureResourcePath(relativePath); + if (!PathFilter.isVisible(pathFilter, relativePath)) { + return false; + } + final Path path = getRootPath().resolve(manifestEnabled ? toMultiReleaseRelativePath(relativePath) : relativePath); + return Files.exists(path); + } + + @Override + public Path getPath(String relativePath) { + ensureResourcePath(relativePath); + if (!PathFilter.isVisible(pathFilter, relativePath)) { + return null; + } + final Path path = getRootPath().resolve(manifestEnabled ? toMultiReleaseRelativePath(relativePath) : relativePath); + return Files.exists(path) ? path : null; + } + + @Override + public int hashCode() { + return Objects.hash(getContainerPath(), pathFilter, manifestEnabled); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + OpenContainerPathTree other = (OpenContainerPathTree) obj; + return Objects.equals(getContainerPath(), other.getContainerPath()) + && Objects.equals(pathFilter, other.pathFilter) + && manifestEnabled == other.manifestEnabled; + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java index 369f9d7610725..82b10247e824f 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java @@ -82,6 +82,9 @@ static PathTree ofArchive(Path archive, PathFilter filter) { /** * The roots of the path tree. + *

+ * Note that for archives, it will return the path to the archive itself, + * not a path that you can browse. * * @return roots of the path tree */ diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java index ef4871e7f4921..cddce5bfc42a1 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java @@ -57,7 +57,7 @@ public OpenPathTree open() { return new CallerOpenPathTree(lastOpen); } try { - this.lastOpen = new SharedOpenArchivePathTree(openFs()); + this.lastOpen = new SharedOpenArchivePathTree(archive, openFs()); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -68,7 +68,7 @@ private class SharedOpenArchivePathTree extends OpenArchivePathTree { private final AtomicInteger users = new AtomicInteger(1); - protected SharedOpenArchivePathTree(FileSystem fs) { + protected SharedOpenArchivePathTree(Path archivePath, FileSystem fs) { super(fs); openCount.incrementAndGet(); } From ab84f88c674d5fcedadf26bad8e7dc9ba3d19409 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 13 Aug 2024 18:16:27 +0100 Subject: [PATCH 15/22] Add http root to OIDC back channel logout handlers (cherry picked from commit a17b30a4f93f90992ae4822f6f9d3d6a8a8bd10b) --- .../java/io/quarkus/oidc/OidcTenantConfig.java | 2 ++ .../oidc/runtime/BackChannelLogoutHandler.java | 18 ++++++++++++++++-- .../runtime/DefaultTenantConfigResolver.java | 6 ++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index 899c6f454fc4d..0a818c6297b66 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -424,6 +424,8 @@ public void setFrontchannel(Frontchannel frontchannel) { public static class Backchannel { /** * The relative path of the Back-Channel Logout endpoint at the application. + * It must start with the forward slash '/', for example, '/back-channel-logout'. + * This value is always resolved relative to 'quarkus.http.root-path'. */ @ConfigItem public Optional path = Optional.empty(); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java index f3047ddb20521..f66c5899e834f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java @@ -13,6 +13,7 @@ import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.SecurityEvent; import io.quarkus.oidc.SecurityEvent.Type; +import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.security.spi.runtime.SecurityEventHelper; import io.vertx.core.Handler; @@ -24,6 +25,7 @@ public class BackChannelLogoutHandler { private static final Logger LOG = Logger.getLogger(BackChannelLogoutHandler.class); + private static final String SLASH = "/"; @Inject DefaultTenantConfigResolver resolver; @@ -44,7 +46,8 @@ public void setup(@Observes Router router) { private void addRoute(Router router, OidcTenantConfig oidcTenantConfig) { if (oidcTenantConfig.isTenantEnabled() && oidcTenantConfig.logout.backchannel.path.isPresent()) { - router.route(oidcTenantConfig.logout.backchannel.path.get()).handler(new RouteHandler(oidcTenantConfig)); + router.route(getRootPath() + oidcTenantConfig.logout.backchannel.path.get()) + .handler(new RouteHandler(oidcTenantConfig)); } } @@ -160,7 +163,18 @@ private TenantConfigContext getTenantConfigContext(RoutingContext context) { private boolean isMatchingTenant(String requestPath, TenantConfigContext tenant) { return tenant.oidcConfig.isTenantEnabled() && tenant.oidcConfig.getTenantId().get().equals(oidcTenantConfig.getTenantId().get()) - && requestPath.equals(tenant.oidcConfig.logout.backchannel.path.orElse(null)); + && requestPath.equals(getRootPath() + tenant.oidcConfig.logout.backchannel.path.orElse(null)); } } + + private String getRootPath() { + // Prepend '/' if it is not present + String rootPath = OidcCommonUtils.prependSlash(resolver.getRootPath()); + // Strip trailing '/' if the length is > 1 + if (rootPath.length() > 1 && rootPath.endsWith("/")) { + rootPath = rootPath.substring(rootPath.length() - 1); + } + // if it is only '/' then return an empty value + return SLASH.equals(rootPath) ? "" : rootPath; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java index d679739d539b8..28d79053b5132 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java @@ -51,6 +51,7 @@ public class DefaultTenantConfigResolver { private final TenantConfigBean tenantConfigBean; private final TenantResolver[] staticTenantResolvers; private final boolean annotationBasedTenantResolutionEnabled; + private final String rootPath; @Inject Instance tenantConfigResolver; @@ -86,6 +87,7 @@ public class DefaultTenantConfigResolver { this.staticTenantResolvers = prepareStaticTenantResolvers(tenantConfigBean, rootPath, tenantResolverInstance, resolveTenantsWithIssuer, new DefaultStaticTenantResolver()); this.annotationBasedTenantResolutionEnabled = Boolean.getBoolean(OidcUtils.ANNOTATION_BASED_TENANT_RESOLUTION_ENABLED); + this.rootPath = rootPath; } @PostConstruct @@ -414,6 +416,10 @@ public OidcTenantConfig getResolvedConfig(String sessionTenantId) { return null; } + public String getRootPath() { + return rootPath; + } + private static final class IssuerBasedTenantResolver implements TenantResolver { private final TenantConfigContext[] tenantConfigContexts; From c8c37ed6988c2762aa32e2ad3d2969413644bf04 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Wed, 14 Aug 2024 10:18:52 +0100 Subject: [PATCH 16/22] Correct indendation so metadata is in the metadata block (cherry picked from commit 43a3ccbc74f1c89150235dead55d2d031bcbfe40) --- .../src/main/asciidoc/extension-metadata.adoc | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/src/main/asciidoc/extension-metadata.adoc b/docs/src/main/asciidoc/extension-metadata.adoc index 3c66b792d8ad2..220ff69f48033 100644 --- a/docs/src/main/asciidoc/extension-metadata.adoc +++ b/docs/src/main/asciidoc/extension-metadata.adoc @@ -64,6 +64,9 @@ And here is the final version of the file included in the runtime JAR augmented ---- name: "Quarkus REST (formerly RESTEasy Reactive)" artifact: "io.quarkus:quarkus-rest:999-SNAPSHOT" +description: "A Jakarta REST implementation utilizing build time processing and Vert.x.\ + \ This extension is not compatible with the quarkus-resteasy extension, or any of\ + \ the extensions that depend on it." <1> metadata: short-name: "rest" keywords: @@ -84,13 +87,13 @@ metadata: artifact: "io.quarkus:quarkus-project-core-extension-codestarts::jar:999-SNAPSHOT" config: - "quarkus.rest." - built-with-quarkus-core: "3.8.5" <1> - requires-quarkus-core: "[3.8,)" <2> - capabilities: <3> + built-with-quarkus-core: "3.8.5" <2> + requires-quarkus-core: "[3.8,)" <3> + capabilities: <4> provides: - "io.quarkus.rest" - "io.quarkus.resteasy.reactive" - extension-dependencies: <4> + extension-dependencies: <5> - "io.quarkus:quarkus-rest-common" - "io.quarkus:quarkus-mutiny" - "io.quarkus:quarkus-smallrye-context-propagation" @@ -100,21 +103,18 @@ metadata: - "io.quarkus:quarkus-vertx-http" - "io.quarkus:quarkus-core" - "io.quarkus:quarkus-jsonp" -description: "A Jakarta REST implementation utilizing build time processing and Vert.x.\ - \ This extension is not compatible with the quarkus-resteasy extension, or any of\ - \ the extensions that depend on it." <5> -scm-url: "https://github.com/quarkusio/quarkus" <6> -sponsor: A Sponsoring Organisation <7> + scm-url: "https://github.com/quarkusio/quarkus" <6> + sponsor: A Sponsoring Organisation <7> ---- - -<1> Quarkus version the extension was built with -<2> The Quarkus version range this extension requires. Optional, and will be set automatically by using the `built-with-quarkus-core` as the minimum range. -<3> https://quarkus.io/guides/capabilities[Capabilities] this extension provides -<4> Direct dependencies on other extensions -<5> Description that can be displayed to users. In this case, the description was copied from the `pom.xml` of the extension module but it could also be provided in the template file. +<1> Description that can be displayed to users. In this case, the description was copied from the `pom.xml` of the extension module but it could also be provided in the template file. +<2> Quarkus version the extension was built with +<3> The Quarkus version range this extension requires. Optional, and will be set automatically by using the `built-with-quarkus-core` as the minimum range. +<4> https://quarkus.io/guides/capabilities[Capabilities] this extension provides +<5> Direct dependencies on other extensions <6> The source code repository of this extension. Optional, and will often be set automatically by using the `` information in the pom. In GitHub Actions builds, it will be inferred from the CI environment. For other GitHub repositories, it can be controlled by setting a `GITHUB_REPOSITORY` environment variable. <7> The sponsor(s) of this extension. Optional, and will sometimes be determined automatically from commit history. + [[quarkus-extension-properties]] == META-INF/quarkus-extension.properties From 1ca100a3f28207287b4454cf110ae792a317ec2c Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Wed, 14 Aug 2024 11:56:13 +0200 Subject: [PATCH 17/22] Redis cache: make blocking executions unordered When the Redis cache is invoked from an ordered Vert.x blocking execution, which happens for example with SmallRye GraphQL or with chained caching (one blocking `@CacheResult` method invoking other blocking `@CacheResult` method), the Redis cache ends up hanging. This is because the next execution cannot start until the previous execution finishes, but the previous execution waits for the next execution. This commit fixes that by making the Redis cache blocking executions unordered. (cherry picked from commit 1fd2a9a22d7ac265c89766ef81daf603e72d9b3b) --- .../deployment/ChainedRedisCacheTest.java | 49 +++++++++++++++++++ .../cache/redis/runtime/RedisCacheImpl.java | 6 +-- 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ChainedRedisCacheTest.java diff --git a/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ChainedRedisCacheTest.java b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ChainedRedisCacheTest.java new file mode 100644 index 0000000000000..c7cdd51420da0 --- /dev/null +++ b/extensions/redis-cache/deployment/src/test/java/io/quarkus/cache/redis/deployment/ChainedRedisCacheTest.java @@ -0,0 +1,49 @@ +package io.quarkus.cache.redis.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.cache.CacheResult; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class ChainedRedisCacheTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(ChainedCachedService.class, TestUtil.class)); + + @Inject + ChainedCachedService chainedCachedService; + + @Test + public void test() { + RedisDataSource redisDataSource = Arc.container().select(RedisDataSource.class).get(); + List allKeysAtStart = TestUtil.allRedisKeys(redisDataSource); + + assertEquals("fubar:42", chainedCachedService.cache1("fubar")); + + List allKeysAtEnd = TestUtil.allRedisKeys(redisDataSource); + assertEquals(allKeysAtStart.size() + 2, allKeysAtEnd.size()); + } + + @ApplicationScoped + public static class ChainedCachedService { + @CacheResult(cacheName = "cache1") + public String cache1(String key) { + return key + ":" + cache2(42); + } + + @CacheResult(cacheName = "cache2") + public int cache2(int value) { + return value; + } + } +} diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java index f2a414678229c..d0be182e72ad0 100644 --- a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java +++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java @@ -141,7 +141,7 @@ private Uni computeValue(K key, Function valueLoader, boolean is public V get() { return valueLoader.apply(key); } - }).runSubscriptionOn(MutinyHelper.blockingExecutor(vertx.getDelegate())); + }).runSubscriptionOn(MutinyHelper.blockingExecutor(vertx.getDelegate(), false)); } else { return Uni.createFrom().item(valueLoader.apply(key)); } @@ -205,8 +205,8 @@ public Uni apply(V value) { result = set(connection, encodedKey, encodedValue).replaceWith(value); } if (isWorkerThread) { - return result - .runSubscriptionOn(MutinyHelper.blockingExecutor(vertx.getDelegate())); + return result.runSubscriptionOn( + MutinyHelper.blockingExecutor(vertx.getDelegate(), false)); } return result; } From 2b8983f9f079734aaf81cf49c0986d25bacb837f Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 16 Aug 2024 16:51:02 +0200 Subject: [PATCH 18/22] Properly check header before extracting the bearer token Fixes #42591 (cherry picked from commit 51834c5559995f69936c458acd902f4788d7e9fa) --- .../runtime/auth/OAuth2AuthMechanism.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java b/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java index c61b680c29706..ef644ce1007b8 100644 --- a/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java +++ b/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java @@ -24,6 +24,8 @@ @ApplicationScoped public class OAuth2AuthMechanism implements HttpAuthenticationMechanism { + private static final String BEARER_PREFIX = "Bearer "; + protected static final ChallengeData CHALLENGE_DATA = new ChallengeData( HttpResponseStatus.UNAUTHORIZED.code(), HttpHeaderNames.WWW_AUTHENTICATE, @@ -42,15 +44,17 @@ public class OAuth2AuthMechanism implements HttpAuthenticationMechanism { public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { String authHeader = context.request().headers().get("Authorization"); - String bearerToken = authHeader != null ? authHeader.substring(7) : null; - if (bearerToken != null) { - // Install the OAuth2 principal as the caller - return identityProviderManager - .authenticate(new TokenAuthenticationRequest(new TokenCredential(bearerToken, "bearer"))); + if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) { + // No suitable bearer token has been found in this request, + return Uni.createFrom().nullItem(); } - // No suitable header has been found in this request, - return Uni.createFrom().nullItem(); + + String bearerToken = authHeader.substring(BEARER_PREFIX.length()); + + // Install the OAuth2 principal as the caller + return identityProviderManager + .authenticate(new TokenAuthenticationRequest(new TokenCredential(bearerToken, "bearer"))); } @Override From 2ff3b6c824349fe77adb259427753fb586c8f748 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 16 Aug 2024 17:59:59 +0200 Subject: [PATCH 19/22] Fix ElytronOauth2ExtensionResourceTestCase The header should be: Authorization: Bearer and not Authorization: Bearer: (cherry picked from commit cd12e4ca79ad6d4be5ce31409debf2a2be0458be) --- .../oauth2/ElytronOauth2ExtensionResourceTestCase.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/integration-tests/elytron-security-oauth2/src/test/java/io/quarkus/it/elytron/oauth2/ElytronOauth2ExtensionResourceTestCase.java b/integration-tests/elytron-security-oauth2/src/test/java/io/quarkus/it/elytron/oauth2/ElytronOauth2ExtensionResourceTestCase.java index 496863baf8204..3a3797eabb130 100644 --- a/integration-tests/elytron-security-oauth2/src/test/java/io/quarkus/it/elytron/oauth2/ElytronOauth2ExtensionResourceTestCase.java +++ b/integration-tests/elytron-security-oauth2/src/test/java/io/quarkus/it/elytron/oauth2/ElytronOauth2ExtensionResourceTestCase.java @@ -56,7 +56,7 @@ public void authenticated() { ensureStarted(); RestAssured.given() .when() - .header("Authorization", "Bearer: " + BEARER_TOKEN) + .header("Authorization", "Bearer " + BEARER_TOKEN) .get("/api/authenticated") .then() .statusCode(200) @@ -78,7 +78,7 @@ public void forbidden() { ensureStarted(); RestAssured.given() .when() - .header("Authorization", "Bearer: " + BEARER_TOKEN) + .header("Authorization", "Bearer " + BEARER_TOKEN) .get("/api/forbidden") .then() .statusCode(403); @@ -99,13 +99,13 @@ public void testGrpcAuthorization() { ensureStarted(); RestAssured.given() .when() - .header("Authorization", "Bearer: " + BEARER_TOKEN) + .header("Authorization", "Bearer " + BEARER_TOKEN) .get("/api/grpc-writer") .then() .statusCode(500); RestAssured.given() .when() - .header("Authorization", "Bearer: " + BEARER_TOKEN) + .header("Authorization", "Bearer " + BEARER_TOKEN) .get("/api/grpc-reader") .then() .statusCode(200) From cb11e94d235703d65a783f843988f51e003a7fe7 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 16 Aug 2024 14:09:29 +0200 Subject: [PATCH 20/22] Use the root path for OpenContainerPathTree#getRoots() The container path is the path of the container (could be a directory or an archive and shouldn't be used for browsing. The root path is the path of the root. In the case of an archive, it's actually a ZipPath, which can be used to browse the archive. Fixes #42571 (cherry picked from commit 6dd61509ce9925d701fe1589a2304ee07a848400) --- .../src/main/java/io/quarkus/paths/OpenContainerPathTree.java | 2 +- .../app-model/src/main/java/io/quarkus/paths/PathTree.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/OpenContainerPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/OpenContainerPathTree.java index 5def8099b50bc..7ef8b4eed9533 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/OpenContainerPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/OpenContainerPathTree.java @@ -93,7 +93,7 @@ public OpenPathTree open() { @Override public Collection getRoots() { - return List.of(getContainerPath()); + return List.of(getRootPath()); } @Override diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java index 82b10247e824f..b4287841554ff 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java @@ -83,8 +83,7 @@ static PathTree ofArchive(Path archive, PathFilter filter) { /** * The roots of the path tree. *

- * Note that for archives, it will return the path to the archive itself, - * not a path that you can browse. + * Note that you shouldn't use these roots for browsing except if the PathTree is open. * * @return roots of the path tree */ From 10f67033d42cd5041d0c28774633d88e84e2ae4d Mon Sep 17 00:00:00 2001 From: KS Date: Sat, 17 Aug 2024 14:08:09 +0200 Subject: [PATCH 21/22] Fix obsolete csrf extension name and configuration in security-csrf-prevention.adoc (cherry picked from commit 0bb5206b2ae35cf8ae8fc6d14577bd3949afb75d) --- .../main/asciidoc/security-csrf-prevention.adoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/main/asciidoc/security-csrf-prevention.adoc b/docs/src/main/asciidoc/security-csrf-prevention.adoc index 154a53f0ea5e0..57cae15e40d71 100644 --- a/docs/src/main/asciidoc/security-csrf-prevention.adoc +++ b/docs/src/main/asciidoc/security-csrf-prevention.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: security :topics: security,csrf,http -:extensions: io.quarkus:quarkus-csrf-reactive +:extensions: io.quarkus:quarkus-rest-csrf https://owasp.org/www-community/attacks/csrf[Cross-Site Request Forgery (CSRF)] is an attack that forces an end user to execute unwanted actions on a web application in which they are currently authenticated. @@ -24,15 +24,15 @@ First, we need a new project. Create a new project with the following command: :create-app-artifact-id: security-csrf-prevention -:create-app-extensions: csrf-reactive +:create-app-extensions: rest-csrf include::{includes}/devtools/create-app.adoc[] -This command generates a project which imports the `csrf-reactive` extension. +This command generates a project which imports the `rest-csrf` extension. -If you already have your Quarkus project configured, you can add the `csrf-reactive` extension +If you already have your Quarkus project configured, you can add the `rest-csrf` extension to your project by running the following command in your project base directory: -:add-extension-extensions: csrf-reactive +:add-extension-extensions: rest-csrf include::{includes}/devtools/extension-add.adoc[] This will add the following to your build file: @@ -42,14 +42,14 @@ This will add the following to your build file: ---- io.quarkus - quarkus-csrf-reactive + quarkus-rest-csrf ---- [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- -implementation("io.quarkus:quarkus-csrf-reactive") +implementation("io.quarkus:quarkus-rest-csrf") ---- Next, let's add a `csrfToken.html` Qute template producing an HTML form in the `src/main/resources/templates` folder: From 8f9ea62c540c90d2a489d19ccbdb47f2589c14ea Mon Sep 17 00:00:00 2001 From: Falko Modler Date: Fri, 16 Aug 2024 18:41:22 +0200 Subject: [PATCH 22/22] Downgrade commons-lang3 to 3.14.0 to avoid entropy issues Details: https://github.com/quarkusio/quarkus/pull/41962#issuecomment-2293614340 (cherry picked from commit 6ce7deee0c7ee398586c875df4616e8adad2e857) --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 00ea1a94d93c0..d77219d91f3df 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -94,7 +94,7 @@ 1.8.0 2.17.2 1.0.0.Final - 3.15.0 + 3.14.0 1.17.1 1.7.0