diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 6686387b69ecb..5c00790c93943 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -39,7 +39,7 @@ 1.23.1 1.23.0-alpha 1.8.1 - 5.0.1.Final + 5.0.2.Final 1.10.5 2.1.12 0.22.0 @@ -67,7 +67,7 @@ 2.1.0 1.0.13 3.0.0 - 3.2.0 + 3.3.0 4.4.0 2.1.0 2.1.1 @@ -97,10 +97,10 @@ 3.12.0 1.15 1.5.1 - 6.2.0.Final + 6.2.1.Final 1.12.18 6.0.6.Final - 2.0.0.Beta2 + 2.0.0.CR1 8.0.0.Final 6.1.7.Final 6.0.0.Final @@ -119,8 +119,7 @@ 1.0.1.Final 2.1.0.Final 3.5.0.Final - 4.3.7 - + 4.4.1 4.5.14 4.4.16 4.1.5 @@ -142,7 +141,7 @@ 14.0.8.Final 4.6.2.Final 3.1.5 - 4.1.87.Final + 4.1.90.Final 1.11.0 1.0.4 3.5.0.Final @@ -161,7 +160,7 @@ 1.8.10 1.6.4 1.5.0 - 3.5.4 + 3.5.5 3.2.0 4.2.0 1.1.1 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/LaunchModeBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LaunchModeBuildItem.java index 20fad98e87f28..e25cef971c186 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/LaunchModeBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LaunchModeBuildItem.java @@ -43,6 +43,15 @@ public Optional getDevModeType() { return devModeType; } + /** + * Whether the development mode type is not local. + * + * @return true if {@link #getDevModeType()} is not {@link DevModeType#LOCAL} + */ + public boolean isNotLocalDevModeType() { + return devModeType.orElse(null) != DevModeType.LOCAL; + } + /** * An Auxiliary Application is a second application running in the same JVM as a primary application. *

diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java index 111b6171fdba6..c032fce642b7b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java @@ -173,8 +173,6 @@ public final class RunTimeConfigurationGenerator { static final MethodDescriptor CU_ADD_SOURCE_FACTORY_PROVIDER = MethodDescriptor.ofMethod(ConfigUtils.class, "addSourceFactoryProvider", void.class, SmallRyeConfigBuilder.class, ConfigSourceFactoryProvider.class); - static final MethodDescriptor CU_WITH_MAPPING = MethodDescriptor.ofMethod(ConfigUtils.class, "addMapping", - void.class, SmallRyeConfigBuilder.class, String.class, String.class); static final MethodDescriptor RCS_NEW = MethodDescriptor.ofConstructor(RuntimeConfigSource.class, String.class); static final MethodDescriptor RCSP_NEW = MethodDescriptor.ofConstructor(RuntimeConfigSourceProvider.class, String.class); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java index 49d64e6e45251..7e99084f77538 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java @@ -280,9 +280,13 @@ private Path createAppCDSFromExit(JarBuildItem jarResult, Path workingDirectory = appCDSPathsContainer.workingDirectory; Path appCDSPath = appCDSPathsContainer.resultingFile; - List javaArgs = new ArrayList<>(3); + boolean debug = log.isDebugEnabled(); + List javaArgs = new ArrayList<>(debug ? 4 : 3); javaArgs.add("-XX:ArchiveClassesAtExit=" + appCDSPath.getFileName().toString()); javaArgs.add(String.format("-D%s=true", MainClassBuildStep.GENERATE_APP_CDS_SYSTEM_PROPERTY)); + if (debug) { + javaArgs.add("-Xlog:cds=debug"); + } javaArgs.add("-jar"); List command; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java index 29d490c2cc658..48bd549b6edd6 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java @@ -75,6 +75,7 @@ import io.quarkus.runtime.configuration.ConfigRecorder; import io.quarkus.runtime.configuration.DefaultsConfigSource; import io.quarkus.runtime.configuration.DisableableConfigSource; +import io.quarkus.runtime.configuration.MappingsConfigBuilder; import io.quarkus.runtime.configuration.QuarkusConfigValue; import io.quarkus.runtime.configuration.RuntimeOverrideConfigSource; import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; @@ -91,10 +92,6 @@ public class ConfigGenerationBuildStep { SmallRyeConfigBuilder.class, "withSources", SmallRyeConfigBuilder.class, ConfigSource[].class); - private static final MethodDescriptor WITH_MAPPING = MethodDescriptor.ofMethod( - SmallRyeConfigBuilder.class, "withMapping", - SmallRyeConfigBuilder.class, Class.class, String.class); - @BuildStep void staticInitSources( BuildProducer staticInitConfigSourceProviderBuildItem, @@ -553,23 +550,25 @@ private static void generateMappingsConfigBuilder( .classOutput(new GeneratedClassGizmoAdaptor(generatedClass, true)) .className(className) .interfaces(ConfigBuilder.class) + .superClass(MappingsConfigBuilder.class) .setFinal(true) .build()) { MethodCreator method = classCreator.getMethodCreator(CONFIG_BUILDER); ResultHandle configBuilder = method.getMethodParam(0); + MethodDescriptor addMapping = MethodDescriptor.ofMethod(MappingsConfigBuilder.class, "addMapping", void.class, + SmallRyeConfigBuilder.class, String.class, String.class); + for (ConfigClassWithPrefix mapping : mappings) { - method.invokeVirtualMethod(WITH_MAPPING, configBuilder, - method.loadClass(mapping.getKlass()), + method.invokeStaticMethod(addMapping, configBuilder, method.load(mapping.getKlass().getName()), method.load(mapping.getPrefix())); } method.returnValue(configBuilder); } - reflectiveClass - .produce(ReflectiveClassBuildItem.builder(className).build()); + reflectiveClass.produce(ReflectiveClassBuildItem.builder(className).build()); } private static Set discoverService( diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java index 26035efd27f36..0e5c0643f18ea 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java @@ -159,8 +159,6 @@ private List recursivelyFindConfigItems(Element element, String r String name = null; String defaultValue = NO_DEFAULT; String defaultValueDoc = EMPTY; - final TypeMirror typeMirror = unwrapTypeMirror(enclosedElement.asType()); - String type = typeMirror.toString(); List acceptedValues = null; final TypeElement clazz = (TypeElement) element; final String fieldName = enclosedElement.getSimpleName().toString(); @@ -250,6 +248,9 @@ private List recursivelyFindConfigItems(Element element, String r defaultValue = EMPTY; } + TypeMirror typeMirror = unwrapTypeMirror(enclosedElement.asType()); + String type = getType(typeMirror); + if (isConfigGroup(type)) { List groupConfigItems = readConfigGroupItems(configPhase, rootName, name, type, configSection, withinAMap, generateSeparateConfigGroupDocsFiles); @@ -387,6 +388,15 @@ private TypeMirror unwrapTypeMirror(TypeMirror typeMirror) { return typeMirror; } + private String getType(TypeMirror typeMirror) { + if (typeMirror instanceof DeclaredType) { + DeclaredType declaredType = (DeclaredType) typeMirror; + TypeElement typeElement = (TypeElement) declaredType.asElement(); + return typeElement.getQualifiedName().toString(); + } + return typeMirror.toString(); + } + private boolean isConfigGroup(String type) { if (type.startsWith("java.") || PRIMITIVE_TYPES.contains(type)) { return false; diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java index 7169fceea750e..649a612ed8433 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java @@ -244,15 +244,6 @@ public static void addSourceFactoryProvider(SmallRyeConfigBuilder builder, Confi builder.withSources(provider.getConfigSourceFactory(Thread.currentThread().getContextClassLoader())); } - public static void addMapping(SmallRyeConfigBuilder builder, String mappingClass, String prefix) { - ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); - try { - builder.withMapping(contextClassLoader.loadClass(mappingClass), prefix); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - public static List getProfiles() { return ConfigProvider.getConfig().unwrap(SmallRyeConfig.class).getProfiles(); } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/MappingsConfigBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/MappingsConfigBuilder.java new file mode 100644 index 0000000000000..24d1b607074be --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/MappingsConfigBuilder.java @@ -0,0 +1,18 @@ +package io.quarkus.runtime.configuration; + +import io.smallrye.config.SmallRyeConfigBuilder; + +/** + * To support mappings that are not public + */ +public abstract class MappingsConfigBuilder implements ConfigBuilder { + protected static void addMapping(SmallRyeConfigBuilder builder, String mappingClass, String prefix) { + // TODO - Ideally should use the classloader passed to Config, but the method is not public. Requires a change in SmallRye Config. + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + builder.withMapping(contextClassLoader.loadClass(mappingClass), prefix); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/init/InitializationTaskRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/init/InitializationTaskRecorder.java index 5a095f3d15ddf..752efe3c9fcfa 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/init/InitializationTaskRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/init/InitializationTaskRecorder.java @@ -1,10 +1,14 @@ package io.quarkus.runtime.init; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.stream.StreamSupport; import org.eclipse.microprofile.config.ConfigProvider; import io.quarkus.runtime.PreventFurtherStepsException; +import io.quarkus.runtime.Quarkus; import io.quarkus.runtime.annotations.Recorder; /** @@ -22,8 +26,27 @@ public void exitIfNeeded() { .anyMatch(n -> QUARKUS_INIT_AND_EXIT.equals(n)); if (initAndExitConfigured) { if (ConfigProvider.getConfig().getValue(QUARKUS_INIT_AND_EXIT, boolean.class)) { - throw new PreventFurtherStepsException("Gracefully exiting after initalization.", 0); + preventFurtherRecorderSteps(5, "Error attempting to gracefully shutdown after initialization", + () -> new PreventFurtherStepsException("Gracefully exiting after initialization.", 0)); } } } + + public static void preventFurtherRecorderSteps(int waitSeconds, String waitErrorMessage, + Supplier supplier) { + CountDownLatch latch = new CountDownLatch(1); + new Thread(new Runnable() { + @Override + public void run() { + Quarkus.blockingExit(); + latch.countDown(); + } + }).start(); + try { + latch.await(waitSeconds, TimeUnit.SECONDS); + } catch (InterruptedException e) { + System.err.println(waitErrorMessage); + } + throw supplier.get(); + } } diff --git a/docs/src/main/asciidoc/config-extending-support.adoc b/docs/src/main/asciidoc/config-extending-support.adoc index ae4f9035d3586..80a84b2fa5aec 100644 --- a/docs/src/main/asciidoc/config-extending-support.adoc +++ b/docs/src/main/asciidoc/config-extending-support.adoc @@ -368,6 +368,8 @@ The `ConfigSourceInterceptorFactory` may initialize an interceptor with access t ---- package org.acme.config; +import static io.smallrye.config.SecretKeys.doLocked; + import jakarta.annotation.Priority; import io.smallrye.config.ConfigSourceInterceptor; diff --git a/docs/src/main/asciidoc/doc-reference.adoc b/docs/src/main/asciidoc/doc-reference.adoc index 907359f0952c6..0c8774698d302 100644 --- a/docs/src/main/asciidoc/doc-reference.adoc +++ b/docs/src/main/asciidoc/doc-reference.adoc @@ -57,7 +57,7 @@ Your titles and headings must also follow the specific guidance for the Quarkus .Title guidance for different Quarkus content types [cols="15%,25%a,30%,30%"] |=== -|Content type |Should ... |Good example|Bad example +|Content type |Should ... |Good example |Bad example |Concept |* Start with a noun that names the concept or topic diff --git a/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc b/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc index 437e49d356470..f554c6e4fb5a7 100644 --- a/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc +++ b/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc @@ -622,7 +622,7 @@ For example, with Apicurio dev service if you set the image name to use version [source,properties] ---- -quarkus.apicurio-registry.devservices.image-name=quay.io/apicurio/apicurio-registry-mem:2.4.2.Final +quarkus.apicurio-registry.devservices.image-name=quay.io/apicurio/apicurio-registry-mem:2.1.5.Final ---- You need to make sure that `apicurio-registry-serdes-avro-serde` dependency @@ -645,6 +645,16 @@ and the REST client `apicurio-common-rest-client-vertx` dependency are set to co + + io.apicurio + apicurio-registry-client + 2.1.5.Final + + + io.apicurio + apicurio-registry-common + 2.1.5.Final + io.apicurio apicurio-registry-serdes-avro-serde @@ -654,6 +664,14 @@ and the REST client `apicurio-common-rest-client-vertx` dependency are set to co io.apicurio apicurio-common-rest-client-jdk + + io.apicurio + apicurio-registry-client + + + io.apicurio + apicurio-registry-common + @@ -674,6 +692,18 @@ dependencies { implementation("io.quarkus:quarkus-apicurio-registry-avro") implementation("io.apicurio:apicurio-registry-serdes-avro-serde") { exclude group: "io.apicurio", module: "apicurio-common-rest-client-jdk" + exclude group: "io.apicurio", module: "apicurio-registry-client" + exclude group: "io.apicurio", module: "apicurio-registry-common" + version { + strictly "2.1.5.Final" + } + } + implementation("io.apicurio:apicurio-registry-client") { + version { + strictly "2.1.5.Final" + } + } + implementation("io.apicurio:apicurio-registry-common") { version { strictly "2.1.5.Final" } @@ -686,6 +716,11 @@ dependencies { } ---- +Known previous compatible versions for `apicurio-registry-client` and `apicurio-common-rest-client-vertx` are the following + +- `apicurio-registry-client` 2.1.5.Final with `apicurio-common-rest-client-vertx` 0.1.5.Final +- `apicurio-registry-client` 2.3.1.Final with `apicurio-common-rest-client-vertx` 0.1.13.Final + [[confluent]] == Using the Confluent Schema Registry diff --git a/docs/src/main/asciidoc/mongodb-panache.adoc b/docs/src/main/asciidoc/mongodb-panache.adoc index 2bf957cecf485..c9821898e0afb 100644 --- a/docs/src/main/asciidoc/mongodb-panache.adoc +++ b/docs/src/main/asciidoc/mongodb-panache.adoc @@ -622,7 +622,7 @@ For these operations, you can express the update document the same way you expre - `firstname = ?1 and status = ?2` will be mapped to the update document `{'$set' : {'firstname': ?1, 'status': ?2}}` - `firstname = :firstname and status = :status` will be mapped to the update document `{'$set' : {'firstname': :firstname, 'status': :status}}` - `{'firstname' : ?1 and 'status' : ?2}` will be mapped to the update document `{'$set' : {'firstname': ?1, 'status': ?2}}` -- `{'firstname' : :firstname and 'status' : :status}` ` will be mapped to the update document `{'$set' : {'firstname': :firstname, 'status': :status}}` +- `{'firstname' : :firstname and 'status' : :status}` will be mapped to the update document `{'$set' : {'firstname': :firstname, 'status': :status}}` - `{'$inc': {'cpt': ?1}}` will be used as-is === Query parameters diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 791aea44d13e4..93ad61a3840cd 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1665,7 +1665,7 @@ On the other hand, if the value is set (e.g. via `TemplateInstance.data("foo", " The type of a default value must be assignable to the type of the parameter declaration. For example, see the incorrect parameter declaration that results in a build failure: `{@org.acme.Foo foo=1}`. TIP: The default value is actually an <>. So the default value does not have to be a literal (such as `42` or `true`). For example, you can leverage the `@TemplateEnum` and specify an enum constant as a default value of a parameter declaration: `{@org.acme.MyEnum myEnum=MyEnum:FOO}`. -However, the infix notation is not supported in default values unless the parentheses are used for grouping, e.g. `{@org.acme.Foo foo=(foo1 ?: foo2)}``. +However, the infix notation is not supported in default values unless the parentheses are used for grouping, e.g. `{@org.acme.Foo foo=(foo1 ?: foo2)}`. IMPORTANT: The type of a default value is not validated in <>. diff --git a/docs/src/main/asciidoc/redis-reference.adoc b/docs/src/main/asciidoc/redis-reference.adoc index a9e3d1e882229..55fba59026da1 100644 --- a/docs/src/main/asciidoc/redis-reference.adoc +++ b/docs/src/main/asciidoc/redis-reference.adoc @@ -730,7 +730,7 @@ This behavior is disabled in _prod_ mode, and if you want to import even in prod %prod.quarkus.redis.load-script=import.redis ---- -Before importing in _prod_ mode, mae sure you configured `quarkus.redis.flush-before-load` accordingly. +Before importing in _prod_ mode, make sure you configured `quarkus.redis.flush-before-load` accordingly. IMPORTANT: In dev mode, to reload the content of the `.redis` load scripts, you need to add: `%dev.quarkus.vertx.caching=false` diff --git a/docs/src/main/asciidoc/rest-client-multipart.adoc b/docs/src/main/asciidoc/rest-client-multipart.adoc index f952569434078..e4d04f8784e7b 100644 --- a/docs/src/main/asciidoc/rest-client-multipart.adoc +++ b/docs/src/main/asciidoc/rest-client-multipart.adoc @@ -153,7 +153,7 @@ The name of the property needs to follow a certain convention which is best disp quarkus.rest-client."org.acme.rest.client.multipart.MultipartService".url=http://localhost:8080/ ---- -Having this configuration means that all requests performed using `org.acme.rest.client.multipart.MultipartService` will use `http://localhost:8080/ ` as the base URL. +Having this configuration means that all requests performed using `org.acme.rest.client.multipart.MultipartService` will use `http://localhost:8080/` as the base URL. Note that `org.acme.rest.client.multipart.MultipartService` _must_ match the fully qualified name of the `MultipartService` interface we created in the previous section. diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index b0f6dded8e37e..531742043a161 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -985,6 +985,68 @@ Naturally this handling is per REST Client. `@ClientExceptionMapper` uses the de NOTE: Methods annotated with `@ClientExceptionMapper` can also take a `java.lang.reflect.Method` parameter which is useful if the exception mapping code needs to know the REST Client method that was invoked and caused the exception mapping code to engage. +=== Using @Blocking annotation in exception mappers + +In cases that warrant using `InputStream` as the return type of REST Client method (such as when large amounts of data need to be read): + +[source, java] +---- +@Path("/echo") +@RegisterRestClient +public interface EchoClient { + + @GET + InputStream get(); +} +---- + +This will work as expected, but if you try to read this InputStream object in a custom exception mapper, you will receive a `BlockingNotAllowedException` exception. This is because `ResponseExceptionMapper` classes are run on the Event Loop thread executor by default - which does not allow to perform IO operations. + +To make your exception mapper blocking, you can annotate the exception mapper with the `@Blocking` annotation: + +[source, java] +---- +@Provider +@Blocking <1> +public class MyResponseExceptionMapper implements ResponseExceptionMapper { + + @Override + public RuntimeException toThrowable(Response response) { + if (response.getStatus() == 500) { + response.readEntity(String.class); <2> + return new RuntimeException("The remote service responded with HTTP 500"); + } + return null; + } +} +---- + +<1> With the `@Blocking` annotation, the MyResponseExceptionMapper exception mapper will be executed in the worker thread pool. +<2> Reading the entity is now allowed because we're executing the mapper on the worker thread pool. + +Note that you can also use the `@Blocking` annotation when using @ClientExceptionMapper: + +[source, java] +---- +@Path("/echo") +@RegisterRestClient +public interface EchoClient { + + @GET + InputStream get(); + + @ClientExceptionMapper + @Blocking + static RuntimeException toException(Response response) { + if (response.getStatus() == 500) { + response.readEntity(String.class); + return new RuntimeException("The remote service responded with HTTP 500"); + } + return null; + } +} +---- + [[multipart]] == Multipart Form support diff --git a/docs/src/main/asciidoc/security-authentication-mechanisms-concept.adoc b/docs/src/main/asciidoc/security-authentication-mechanisms-concept.adoc index c61f195a26d37..8b11df8722087 100644 --- a/docs/src/main/asciidoc/security-authentication-mechanisms-concept.adoc +++ b/docs/src/main/asciidoc/security-authentication-mechanisms-concept.adoc @@ -195,11 +195,11 @@ For more information about OIDC authentication and authorization methods you can [options="header"] |==== |OIDC topic |Quarkus information resource -|Bearer token authentication mechanism|xref:security-oidc-bearer-authentication-concept.adoc[OIDC Bearer authentication] -|Authorization code flow authentication mechanism|xref:security-oidc-code-flow-authentication-concept.adoc[OpenID Connect (OIDC) authorization code flow mechanism] -|Multiple tenants that can support bearer token or authorization code flow mechanisms|xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect (OIDC) multi-tenancy] -|Using Keycloak to centralize authorization|xref:security-keycloak-authorization.adoc[Using OpenID Connect (OIDC) and Keycloak to centralize authorization] -|Configuring Keycloak programmatically|xref:security-keycloak-admin-client.adoc[Using the Keycloak admin client] +|Bearer token authentication mechanism |xref:security-oidc-bearer-authentication-concept.adoc[OIDC Bearer authentication] +|Authorization code flow authentication mechanism |xref:security-oidc-code-flow-authentication-concept.adoc[OpenID Connect (OIDC) authorization code flow mechanism] +|Multiple tenants that can support bearer token or authorization code flow mechanisms |xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect (OIDC) multi-tenancy] +|Using Keycloak to centralize authorization |xref:security-keycloak-authorization.adoc[Using OpenID Connect (OIDC) and Keycloak to centralize authorization] +|Configuring Keycloak programmatically |xref:security-keycloak-admin-client.adoc[Using the Keycloak admin client] |==== [NOTE] 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 626da1b83a4e4..06c36042845f8 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -721,8 +721,8 @@ quarkus.oidc.client.tls.key-store-password=${key-store-password} #quarkus.oidc.client.tls.key-store-alias-password=keyAliasPassword # Truststore configuration -quarkus.oidc.client.tls.trust-store-file=client-truststore.jks -quarkus.oidc.client.tls.trust-store-password=${trust-store-password} +quarkus.oidc-client.tls.trust-store-file=client-truststore.jks +quarkus.oidc-client.tls.trust-store-password=${trust-store-password} # Add more truststore properties if needed: #quarkus.oidc.client.tls.trust-store-alias=certAlias ---- diff --git a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc index e3f4fba9d2475..a2f21633539e0 100644 --- a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc @@ -211,7 +211,7 @@ quarkus.http.auth.permission.authenticated.paths=/* quarkus.http.auth.permission.authenticated.policy=authenticated ---- -The first configuration is the default tenant configuration that should be used when the tenant can not be inferred from the request. Note that a `%prod` prodile prefix is used with `quarkus.oidc.auth-server-url` - it is done to support testing a multi-tenant application with `Dev Services For Keycloak`. This configuration is using a Keycloak instance to authenticate users. +The first configuration is the default tenant configuration that should be used when the tenant can not be inferred from the request. Note that a `%prod` profile prefix is used with `quarkus.oidc.auth-server-url` - it is done to support testing a multi-tenant application with `Dev Services For Keycloak`. This configuration is using a Keycloak instance to authenticate users. The second configuration is provided by `TenantConfigResolver`, it is the configuration that will be used when an incoming request is mapped to the tenant `tenant-a`. diff --git a/docs/src/main/asciidoc/tests-with-coverage.adoc b/docs/src/main/asciidoc/tests-with-coverage.adoc index 5c6e46bf6f38b..0cda4d5b29bf3 100644 --- a/docs/src/main/asciidoc/tests-with-coverage.adoc +++ b/docs/src/main/asciidoc/tests-with-coverage.adoc @@ -179,6 +179,11 @@ There are some config options that affect this: include::{generated-dir}/config/quarkus-jacoco-jacoco-config.adoc[opts=optional, leveloffset=+1] +[TIP] +==== +When working with a multi-module project, then for code coverage to work properly, the upstream modules need to be properly xref:cdi-reference.adoc#bean_discovery[indexed]. +==== + == Coverage for tests not using @QuarkusTest The Quarkus automatic JaCoCo config will only work for tests that are annotated with `@QuarkusTest`. If you want to check diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/InterceptorResolverBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/InterceptorResolverBuildItem.java index b9d0770a10f58..abfd5c917a9c6 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/InterceptorResolverBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/InterceptorResolverBuildItem.java @@ -1,9 +1,11 @@ package io.quarkus.arc.deployment; +import java.util.Collection; import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; +import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -17,13 +19,14 @@ public final class InterceptorResolverBuildItem extends SimpleBuildItem { private final InterceptorResolver resolver; - private final Set interceptorBindings; + private final BeanDeployment beanDeployment; InterceptorResolverBuildItem(BeanDeployment beanDeployment) { this.resolver = beanDeployment.getInterceptorResolver(); this.interceptorBindings = Collections.unmodifiableSet( - beanDeployment.getInterceptorBindings().stream().map(ClassInfo::name).collect(Collectors.toSet())); + beanDeployment.getInterceptorBindings().stream().map(ClassInfo::name).collect(Collectors.toUnmodifiableSet())); + this.beanDeployment = beanDeployment; } public InterceptorResolver get() { @@ -38,4 +41,14 @@ public Set getInterceptorBindings() { return interceptorBindings; } + /** + * + * @param annotation + * @return the collection of interceptor bindings + * @see BeanDeployment#extractInterceptorBindings(AnnotationInstance) + */ + public Collection extractInterceptorBindings(AnnotationInstance annotation) { + return beanDeployment.extractInterceptorBindings(annotation); + } + } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/ArcDevConsoleProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/ArcDevConsoleProcessor.java index df0b3ce4ce5b9..53d1a2b115c78 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/ArcDevConsoleProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/ArcDevConsoleProcessor.java @@ -10,9 +10,6 @@ import java.util.Set; import java.util.stream.Collectors; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.DotName; - import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.ArcConfig; @@ -22,7 +19,6 @@ import io.quarkus.arc.deployment.ValidationPhaseBuildItem; import io.quarkus.arc.deployment.devconsole.DependencyGraph.Link; import io.quarkus.arc.deployment.devui.ArcBeanInfoBuildItem; -import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.BeanDeploymentValidator; import io.quarkus.arc.processor.BeanDeploymentValidator.ValidationContext; import io.quarkus.arc.processor.BeanInfo; @@ -35,10 +31,7 @@ import io.quarkus.arc.runtime.ArcContainerSupplier; import io.quarkus.arc.runtime.ArcRecorder; import io.quarkus.arc.runtime.BeanLookupSupplier; -import io.quarkus.arc.runtime.devconsole.InvocationInterceptor; -import io.quarkus.arc.runtime.devconsole.InvocationTree; import io.quarkus.arc.runtime.devconsole.InvocationsMonitor; -import io.quarkus.arc.runtime.devconsole.Monitored; import io.quarkus.arc.runtime.devmode.EventsMonitor; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; @@ -71,37 +64,9 @@ void monitor(ArcConfig config, BuildProducer skipNames = new HashSet<>(); - skipNames.add(DotName.createSimple(InvocationTree.class.getName())); - skipNames.add(DotName.createSimple(InvocationsMonitor.class.getName())); - skipNames.add(DotName.createSimple(EventsMonitor.class.getName())); - annotationTransformers.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { - @Override - public void transform(TransformationContext transformationContext) { - if (transformationContext.isClass()) { - ClassInfo beanClass = transformationContext.getTarget().asClass(); - if ((customScopes.isScopeDeclaredOn(beanClass) - || isAdditionalBeanDefiningAnnotationOn(beanClass, beanDefiningAnnotations)) - && !skipNames.contains(beanClass.name())) { - transformationContext.transform().add(Monitored.class).done(); - } - } - } - })); runtimeInfos.produce(new DevConsoleRuntimeTemplateInfoBuildItem("invocationsMonitor", new BeanLookupSupplier(InvocationsMonitor.class), this.getClass(), curateOutcomeBuildItem)); } @@ -226,16 +191,6 @@ public void handle(Void ignore) { static final int DEFAULT_MAX_DEPENDENCY_LEVEL = 10; - private boolean isAdditionalBeanDefiningAnnotationOn(ClassInfo beanClass, - List beanDefiningAnnotations) { - for (BeanDefiningAnnotationBuildItem beanDefiningAnnotation : beanDefiningAnnotations) { - if (beanClass.classAnnotation(beanDefiningAnnotation.getName()) != null) { - return true; - } - } - return false; - } - DependencyGraph buildDependencyGraph(BeanInfo bean, ValidationContext validationContext, BeanResolver resolver, DevBeanInfos devBeanInfos, List allInjectionPoints, Map> declaringToProducers, diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcDevUIProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcDevUIProcessor.java index c76dac830e220..0aeb3f4ab27e2 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcDevUIProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcDevUIProcessor.java @@ -2,15 +2,30 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.ArcConfig; +import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem; +import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem; import io.quarkus.arc.deployment.devconsole.DevBeanInfo; import io.quarkus.arc.deployment.devconsole.DevBeanInfos; import io.quarkus.arc.deployment.devconsole.DevDecoratorInfo; import io.quarkus.arc.deployment.devconsole.DevInterceptorInfo; import io.quarkus.arc.deployment.devconsole.DevObserverInfo; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.runtime.devconsole.InvocationInterceptor; +import io.quarkus.arc.runtime.devconsole.InvocationTree; +import io.quarkus.arc.runtime.devconsole.InvocationsMonitor; +import io.quarkus.arc.runtime.devconsole.Monitored; +import io.quarkus.arc.runtime.devmode.EventsMonitor; import io.quarkus.arc.runtime.devui.ArcJsonRPCService; import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; import io.quarkus.devui.spi.page.CardPageBuildItem; @@ -94,6 +109,46 @@ JsonRPCProvidersBuildItem createJsonRPCService() { return new JsonRPCProvidersBuildItem(ArcJsonRPCService.class); } + @BuildStep(onlyIf = IsDevelopment.class) + void registerMonitoringComponents(ArcConfig config, BuildProducer beans, + BuildProducer annotationTransformers, + CustomScopeAnnotationsBuildItem customScopes, List beanDefiningAnnotations) { + if (!config.devMode.monitoringEnabled) { + return; + } + if (!config.transformUnproxyableClasses) { + throw new IllegalStateException( + "Dev UI problem: monitoring of CDI business method invocations not possible\n\t- quarkus.arc.transform-unproxyable-classes was set to false and therefore it would not be possible to apply interceptors to unproxyable bean classes\n\t- please disable the monitoring feature via quarkus.arc.dev-mode.monitoring-enabled=false or enable unproxyable classes transformation"); + } + // Register beans + beans.produce(AdditionalBeanBuildItem.builder().setUnremovable() + .addBeanClasses(EventsMonitor.class, InvocationTree.class, InvocationsMonitor.class, + InvocationInterceptor.class, + Monitored.class) + .build()); + + // Add @Monitored to all beans + Set skipNames = Set.of(DotName.createSimple(InvocationTree.class), + DotName.createSimple(InvocationsMonitor.class), DotName.createSimple(EventsMonitor.class)); + annotationTransformers.produce(new AnnotationsTransformerBuildItem(AnnotationsTransformer + .appliedToClass() + .whenClass(c -> (customScopes.isScopeDeclaredOn(c) + || isAdditionalBeanDefiningAnnotationOn(c, beanDefiningAnnotations)) + && !skipClass(c, skipNames)) + .thenTransform(t -> t.add(Monitored.class)))); + } + + private boolean skipClass(ClassInfo beanClass, Set skipNames) { + if (skipNames.contains(beanClass.name())) { + return true; + } + if (beanClass.name().packagePrefix().startsWith("io.quarkus.devui.runtime")) { + // Skip monitoring for internal devui components + return true; + } + return false; + } + private List toDevBeanWithInterceptorInfo(List beans, DevBeanInfos devBeanInfos) { List l = new ArrayList<>(); for (DevBeanInfo dbi : beans) { @@ -102,6 +157,16 @@ private List toDevBeanWithInterceptorInfo(List beanDefiningAnnotations) { + for (BeanDefiningAnnotationBuildItem beanDefiningAnnotation : beanDefiningAnnotations) { + if (beanClass.hasDeclaredAnnotation(beanDefiningAnnotation.getName())) { + return true; + } + } + return false; + } + private static final String BEANS = "beans"; private static final String OBSERVERS = "observers"; private static final String INTERCEPTORS = "interceptors"; diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java index 568036a05db26..6e5f32ac07e65 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java @@ -87,9 +87,7 @@ void collectInterceptedStaticMethods(BeanArchiveIndexBuildItem beanArchiveIndex, BuildProducer interceptedStaticMethods, InterceptorResolverBuildItem interceptorResolver, TransformedAnnotationsBuildItem transformedAnnotations, BuildProducer unremovableBeans) { - // In this step we collect all intercepted static methods, i.e. static methods annotated with interceptor bindings - Set interceptorBindings = interceptorResolver.getInterceptorBindings(); for (ClassInfo clazz : beanArchiveIndex.getIndex().getKnownClasses()) { for (MethodInfo method : clazz.methods()) { @@ -107,12 +105,15 @@ void collectInterceptedStaticMethods(BeanArchiveIndexBuildItem beanArchiveIndex, // Only method-level bindings are considered due to backwards compatibility Set methodLevelBindings = null; for (AnnotationInstance annotationInstance : annotations) { - if (annotationInstance.target().kind() == Kind.METHOD - && interceptorBindings.contains(annotationInstance.name())) { - if (methodLevelBindings == null) { - methodLevelBindings = new HashSet<>(); + if (annotationInstance.target().kind() == Kind.METHOD) { + Collection bindings = interceptorResolver + .extractInterceptorBindings(annotationInstance); + if (!bindings.isEmpty()) { + if (methodLevelBindings == null) { + methodLevelBindings = new HashSet<>(); + } + methodLevelBindings.addAll(bindings); } - methodLevelBindings.add(annotationInstance); } } if (methodLevelBindings == null || methodLevelBindings.isEmpty()) { diff --git a/extensions/arc/deployment/src/main/resources/dev-templates/invocations.html b/extensions/arc/deployment/src/main/resources/dev-templates/invocations.html index 320bf44699109..69f7a8c6190c4 100644 --- a/extensions/arc/deployment/src/main/resources/dev-templates/invocations.html +++ b/extensions/arc/deployment/src/main/resources/dev-templates/invocations.html @@ -79,7 +79,7 @@ - {#each info:invocationsMonitor.lastInvocations.orEmpty} + {#each info:invocationsMonitor.filteredLastInvocations.orEmpty} {it.startFormatted}

diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-fired-events.js b/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-fired-events.js index 8185976e9a23b..64fdf4ccd1328 100644 --- a/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-fired-events.js +++ b/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-fired-events.js @@ -3,7 +3,6 @@ import { until } from 'lit/directives/until.js'; import { JsonRpc } from 'jsonrpc'; import '@vaadin/grid'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; -import '@vaadin/details'; import '@vaadin/vertical-layout'; import '@vaadin/button'; import '@vaadin/checkbox'; @@ -28,27 +27,29 @@ export class QwcArcFiredEvents extends LitElement { .arctable { height: 100%; padding-bottom: 10px; - } - .payload { - color: grey; - font-size: small; }`; static properties = { _firedEvents: {state: true}, - _observer: {state:false}, + _skipLifecycleEvents: {state: true} }; connectedCallback() { super.connectedCallback(); this._refresh(); - this._observer = this.jsonRpc.streamEvents().onNext(jsonRpcResponse => { + this._eventsStream = this.jsonRpc.streamEvents().onNext(jsonRpcResponse => { this._addToEvents(jsonRpcResponse.result); }); + // Context lifecycle events are skipped by default; updates are handled by the stream + this._skipLifecycleEvents = true; + this._skipLifecycleEventsStream = this.jsonRpc.streamSkipContextEvents().onNext(jsonRpcResponse => { + this._skipLifecycleEvents = jsonRpcResponse.result; + }); } disconnectedCallback() { - this._observer.cancel(); + this._eventsStream.cancel(); + this._skipLifecycleEventsStream.cancel(); super.disconnectedCallback(); } @@ -59,13 +60,13 @@ export class QwcArcFiredEvents extends LitElement { _renderFiredEvents(){ if(this._firedEvents){ return html` @@ -90,40 +91,34 @@ export class QwcArcFiredEvents extends LitElement { } } - _payloadRenderer(event) { + _eventTypeRenderer(event) { return html` - -
${event.type}
- - - ${event.payload} - -
+ ${event.type} `; } _refresh(){ - console.log("refresh"); this.jsonRpc.getLastEvents().then(events => { this._firedEvents = events.result; }); } _clear(){ - console.log("clear"); this.jsonRpc.clearLastEvents().then(events => { this._firedEvents = events.result; }); } _toggleContext(){ - console.log("context"); + this.jsonRpc.toggleSkipContextEvents().then(events => { + this._firedEvents = events.result; + }); } _addToEvents(event){ this._firedEvents = [ - ...this._firedEvents, event, + ...this._firedEvents, ]; } } diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-invocation-trees.js b/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-invocation-trees.js index 0e3e18d482aca..1a5dc198f04ea 100644 --- a/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-invocation-trees.js +++ b/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-invocation-trees.js @@ -1,8 +1,8 @@ import { LitElement, html, css} from 'lit'; import { until } from 'lit/directives/until.js'; import { JsonRpc } from 'jsonrpc'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; import '@vaadin/grid'; -import '@vaadin/grid/vaadin-grid-tree-column.js'; import '@vaadin/button'; import '@vaadin/checkbox'; @@ -26,14 +26,26 @@ export class QwcArcInvocationTrees extends LitElement { .arctable { height: 100%; padding-bottom: 10px; - }`; + } + ul li::before { + content: "└─ "; + } + ul { + list-style: none; + } + code { + font-size: 90%; + } + `; static properties = { - _invocations: {state: true} + _invocations: {state: true}, + _filterOutQuarkusBeans: {state: true} }; connectedCallback() { super.connectedCallback(); + this._filterOutQuarkusBeans = true; this._refresh(); } @@ -44,13 +56,13 @@ export class QwcArcInvocationTrees extends LitElement { _renderInvocations(){ if(this._invocations){ return html` - + + `; } } + _invocationsRenderer(invocation) { + return html` + + `; + } + + _invocationRenderer(invocation) { + return html` +
  • + ${invocation.kind.toLowerCase()} + ${invocation.methodName} + ${invocation.duration == 0 ? '< 1' : invocation.duration} ms +
      + ${invocation.children.map(child => + html`${this._invocationRenderer(child)}` + )} +
    +
  • + `; + } + _refresh(){ - console.log("refresh"); this.jsonRpc.getLastInvocations().then(invocations => { - this._invocations = invocations.result; + if (this._filterOutQuarkusBeans) { + this._invocations = invocations.result.filter(i => !i.quarkusBean); + } else { + this._invocations = invocations.result; + } }); } _clear(){ - console.log("clear"); this.jsonRpc.clearLastInvocations().then(invocations => { this._invocations = invocations.result; }); } _toggleFilter(){ - console.log("filter"); + this._filterOutQuarkusBeans = !this._filterOutQuarkusBeans; + this._refresh(); } } customElements.define('qwc-arc-invocation-trees', QwcArcInvocationTrees); \ No newline at end of file diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/staticmethods/RepeatingBindingStaticMethodTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/staticmethods/RepeatingBindingStaticMethodTest.java new file mode 100644 index 0000000000000..54219d0cf4a2e --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/staticmethods/RepeatingBindingStaticMethodTest.java @@ -0,0 +1,85 @@ +package io.quarkus.arc.test.interceptor.staticmethods; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InterceptorBinding; +import jakarta.interceptor.InvocationContext; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class RepeatingBindingStaticMethodTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(InterceptMe.class, SimpleBean.class, InterceptMeAlpha.class, InterceptMeBravo.class)); + + @Test + public void testInterceptor() { + assertEquals("a/b/PONG/b/a", SimpleBean.ping("pong")); + } + + public static class SimpleBean { + + @InterceptMe("alpha") + @InterceptMe("bravo") + public static String ping(String val) { + return val.toUpperCase(); + } + } + + @Priority(1) + @Interceptor + @InterceptMe("alpha") + static class InterceptMeAlpha { + + @AroundInvoke + Object aroundInvoke(InvocationContext ctx) throws Exception { + return "a/" + ctx.proceed() + "/a"; + } + + } + + @Priority(2) + @Interceptor + @InterceptMe("bravo") + static class InterceptMeBravo { + + @AroundInvoke + Object aroundInvoke(InvocationContext ctx) throws Exception { + return "b/" + ctx.proceed() + "/b"; + } + + } + + @Repeatable(InterceptMe.List.class) + @InterceptorBinding + @Target({ TYPE, METHOD, CONSTRUCTOR }) + @Retention(RUNTIME) + @interface InterceptMe { + + String value(); + + @Target({ TYPE, METHOD, CONSTRUCTOR }) + @Retention(RUNTIME) + @interface List { + InterceptMe[] value(); + } + + } + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/appcds/AppCDSRecorder.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/appcds/AppCDSRecorder.java index ae93e5bed02f2..70e03db0d3545 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/appcds/AppCDSRecorder.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/appcds/AppCDSRecorder.java @@ -1,31 +1,17 @@ package io.quarkus.arc.runtime.appcds; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - import io.quarkus.runtime.PreventFurtherStepsException; -import io.quarkus.runtime.Quarkus; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.init.InitializationTaskRecorder; @Recorder public class AppCDSRecorder { public void controlGenerationAndExit() { if (Boolean.parseBoolean(System.getProperty("quarkus.appcds.generate", "false"))) { - CountDownLatch latch = new CountDownLatch(1); - new Thread(new Runnable() { - @Override - public void run() { - Quarkus.blockingExit(); - latch.countDown(); - } - }).start(); - try { - latch.await(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - System.err.println("Unable to properly shutdown Quarkus application when creating AppCDS"); - } - throw new PreventFurtherStepsException(); + InitializationTaskRecorder.preventFurtherRecorderSteps(5, + "Unable to properly shutdown Quarkus application when creating AppCDS", + PreventFurtherStepsException::new); } } } diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Invocation.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Invocation.java index 4ac219e710869..cd301510a341d 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Invocation.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/Invocation.java @@ -94,6 +94,13 @@ public String getPackageName(String name) { return ""; } + public boolean isQuarkusBean() { + if (interceptedBean == null) { + return false; + } + return interceptedBean.getBeanClass().getName().startsWith("io.quarkus"); + } + public enum Kind { BUSINESS, diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationsMonitor.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationsMonitor.java index 61f1189c55d8d..0f25f90f722f6 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationsMonitor.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/InvocationsMonitor.java @@ -28,16 +28,22 @@ void addInvocation(Invocation invocation) { invocations.add(invocation); } - public List getLastInvocations() { - List result = new ArrayList<>(invocations); + // this method should be removed when the Dev UI 1 components are removed + public List getFilteredLastInvocations() { + List result = getLastInvocations(); if (filterOutQuarkusBeans) { for (Iterator it = result.iterator(); it.hasNext();) { Invocation invocation = it.next(); - if (invocation.getInterceptedBean().getBeanClass().getName().startsWith("io.quarkus")) { + if (invocation.isQuarkusBean()) { it.remove(); } } } + return result; + } + + public List getLastInvocations() { + List result = new ArrayList<>(invocations); Collections.reverse(result); return result; } diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventInfo.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventInfo.java index fde1f35d48f42..9ce8f7a265fc4 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventInfo.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventInfo.java @@ -7,21 +7,15 @@ public class EventInfo { private String type; private List qualifiers; private boolean isContextEvent; - private Object payload; public EventInfo() { } public EventInfo(String timestamp, String type, List qualifiers, boolean isContextEvent) { - this(timestamp, type, qualifiers, isContextEvent, null); - } - - public EventInfo(String timestamp, String type, List qualifiers, boolean isContextEvent, Object payload) { this.timestamp = timestamp; this.type = type; this.qualifiers = qualifiers; this.isContextEvent = isContextEvent; - this.payload = payload; } public String getTimestamp() { @@ -56,12 +50,4 @@ public void setIsContextEvent(boolean isContextEvent) { this.isContextEvent = isContextEvent; } - public Object getPayload() { - return payload; - } - - public void setPayload(Object payload) { - this.payload = payload; - } - } diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventsMonitor.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventsMonitor.java index 2d85c7db162fa..94a6ebb2d7819 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventsMonitor.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventsMonitor.java @@ -28,6 +28,7 @@ public class EventsMonitor { private volatile boolean skipContextEvents = true; private final List events = Collections.synchronizedList(new ArrayList<>(DEFAULT_LIMIT)); private final BroadcastProcessor eventsStream = BroadcastProcessor.create(); + private final BroadcastProcessor skipContextEventsStream = BroadcastProcessor.create(); void notify(@Observes Object payload, EventMetadata eventMetadata) { if (skipContextEvents && isContextEvent(eventMetadata)) { @@ -54,6 +55,10 @@ public Multi streamEvents() { return eventsStream; } + public Multi streamSkipContextEvents() { + return skipContextEventsStream; + } + public List getLastEvents() { List result = new ArrayList<>(events); Collections.reverse(result); @@ -67,6 +72,7 @@ public boolean isSkipContextEvents() { public void toggleSkipContextEvents() { // This is not thread-safe but we don't expect concurrent actions from dev ui skipContextEvents = !skipContextEvents; + skipContextEventsStream.onNext(skipContextEvents); } boolean isContextEvent(EventMetadata eventMetadata) { @@ -87,14 +93,8 @@ boolean isContextEvent(EventMetadata eventMetadata) { private EventInfo toEventInfo(Object payload, EventMetadata eventMetadata) { EventInfo eventInfo = new EventInfo(); - - // Timestamp eventInfo.setTimestamp(now()); - - // Type eventInfo.setType(eventMetadata.getType().getTypeName()); - - // Qualifiers List q = new ArrayList<>(); if (eventMetadata.getQualifiers().size() > 1) { for (Annotation qualifier : eventMetadata.getQualifiers()) { @@ -105,13 +105,7 @@ private EventInfo toEventInfo(Object payload, EventMetadata eventMetadata) { } } eventInfo.setQualifiers(q); - - // ContextEvent eventInfo.setIsContextEvent(isContextEvent(eventMetadata)); - - // Payload - eventInfo.setPayload(payload.toString()); - return eventInfo; } diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/InvocationInfo.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/InvocationInfo.java index e681fd05e0af3..acc98f37e7207 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/InvocationInfo.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/InvocationInfo.java @@ -1,9 +1,23 @@ package io.quarkus.arc.runtime.devmode; +import java.util.List; + public class InvocationInfo { private String startTime; + private String methodName; + + // in milliseconds + private long duration; + + // business method, producer, observer, etc. + private String kind; + + private List children; + + private boolean quarkusBean; + public String getStartTime() { return startTime; } @@ -12,4 +26,44 @@ public void setStartTime(String startTime) { this.startTime = startTime; } + public String getMethodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public long getDuration() { + return duration; + } + + public void setDuration(long duration) { + this.duration = duration; + } + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public boolean isQuarkusBean() { + return quarkusBean; + } + + public void setQuarkusBean(boolean quarkusBean) { + this.quarkusBean = quarkusBean; + } + } diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devui/ArcJsonRPCService.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devui/ArcJsonRPCService.java index 4b125b56a1872..996fc1f969dee 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devui/ArcJsonRPCService.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devui/ArcJsonRPCService.java @@ -7,7 +7,9 @@ import java.util.ArrayList; import java.util.List; -import io.quarkus.arc.Arc; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + import io.quarkus.arc.runtime.devconsole.Invocation; import io.quarkus.arc.runtime.devconsole.InvocationsMonitor; import io.quarkus.arc.runtime.devmode.EventInfo; @@ -18,38 +20,50 @@ public class ArcJsonRPCService { + @Inject + Instance eventsMonitor; + + @Inject + Instance invocationsMonitor; + public Multi streamEvents() { - EventsMonitor eventsMonitor = Arc.container().instance(EventsMonitor.class).get(); - if (eventsMonitor != null) { - return eventsMonitor.streamEvents(); - } - return Multi.createFrom().empty(); + return eventsMonitor.isResolvable() ? eventsMonitor.get().streamEvents() : Multi.createFrom().empty(); + } + + public Multi streamSkipContextEvents() { + return eventsMonitor.isResolvable() ? eventsMonitor.get().streamSkipContextEvents() : Multi.createFrom().empty(); } @NonBlocking public List getLastEvents() { - EventsMonitor eventsMonitor = Arc.container().instance(EventsMonitor.class).get(); - if (eventsMonitor != null) { - return eventsMonitor.getLastEvents(); + if (eventsMonitor.isResolvable()) { + return eventsMonitor.get().getLastEvents(); } return List.of(); } @NonBlocking public List clearLastEvents() { - EventsMonitor eventsMonitor = Arc.container().instance(EventsMonitor.class).get(); - if (eventsMonitor != null) { - eventsMonitor.clear(); - return eventsMonitor.getLastEvents(); + if (eventsMonitor.isResolvable()) { + eventsMonitor.get().clear(); + return getLastEvents(); + } + return List.of(); + } + + @NonBlocking + public List toggleSkipContextEvents() { + if (eventsMonitor.isResolvable()) { + eventsMonitor.get().toggleSkipContextEvents(); + return getLastEvents(); } return List.of(); } @NonBlocking public List getLastInvocations() { - InvocationsMonitor invocationsMonitor = Arc.container().instance(InvocationsMonitor.class).get(); - if (invocationsMonitor != null) { - List lastInvocations = invocationsMonitor.getLastInvocations(); + if (invocationsMonitor.isResolvable()) { + List lastInvocations = invocationsMonitor.get().getLastInvocations(); return toInvocationInfos(lastInvocations); } return List.of(); @@ -57,9 +71,8 @@ public List getLastInvocations() { @NonBlocking public List clearLastInvocations() { - InvocationsMonitor invocationsMonitor = Arc.container().instance(InvocationsMonitor.class).get(); - if (invocationsMonitor != null) { - invocationsMonitor.clear(); + if (invocationsMonitor.isResolvable()) { + invocationsMonitor.get().clear(); return getLastInvocations(); } return List.of(); @@ -75,8 +88,13 @@ private List toInvocationInfos(List invocations) { private InvocationInfo toInvocationInfo(Invocation invocation) { InvocationInfo info = new InvocationInfo(); - LocalDateTime starttime = LocalDateTime.ofInstant(Instant.ofEpochMilli(invocation.getStart()), ZoneId.systemDefault()); - info.setStartTime(timeString(starttime)); + info.setStartTime( + timeString(LocalDateTime.ofInstant(Instant.ofEpochMilli(invocation.getStart()), ZoneId.systemDefault()))); + info.setMethodName(invocation.getDeclaringClassName() + "#" + invocation.getMethod().getName()); + info.setDuration(invocation.getDurationMillis()); + info.setKind(invocation.getKind().toString()); + info.setChildren(toInvocationInfos(invocation.getChildren())); + info.setQuarkusBean(invocation.isQuarkusBean()); return info; } diff --git a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java index 489beb17221cb..b7b1f61120dfd 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java +++ b/extensions/container-image/container-image-docker/deployment/src/test/java/io/quarkus/container/image/docker/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java @@ -16,7 +16,7 @@ void testImageWithJava11() { Path path = getPath("openjdk-11-runtime"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-11-runtime:1.14"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-11-runtime:1.15"); assertThat(v.getJavaVersion()).isEqualTo(11); }); } @@ -26,7 +26,7 @@ void testImageWithJava17() { Path path = getPath("openjdk-17-runtime"); var result = sut.determine(path); assertThat(result).hasValueSatisfying(v -> { - assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.14"); + assertThat(v.getBaseImage()).isEqualTo("registry.access.redhat.com/ubi8/openjdk-17-runtime:1.15"); assertThat(v.getJavaVersion()).isEqualTo(17); }); } diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime index 0cbac0dfb5547..242b681bff897 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-11-runtime @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/openjdk-11-runtime:1.14 +FROM registry.access.redhat.com/ubi8/openjdk-11-runtime:1.15 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime index 1238ab8318d4a..ae53314ed64b5 100644 --- a/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime +++ b/extensions/container-image/container-image-docker/deployment/src/test/resources/openjdk-17-runtime @@ -1,5 +1,5 @@ # Use Java 17 base image -FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.14 +FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.15 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java index c5a8fea44e41d..9b1ac89915b6c 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java @@ -16,9 +16,9 @@ public class JibConfig { /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.14} + * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.15} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11-runtime:1.14} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11-runtime:1.15} is used as the default. */ @ConfigItem public Optional baseJvmImage; 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 6b14aa638525a..3bca893339175 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 @@ -89,8 +89,8 @@ public class JibProcessor { private static final IsClassPredicate IS_CLASS_PREDICATE = new IsClassPredicate(); private static final String BINARY_NAME_IN_CONTAINER = "application"; - private static final String JAVA_17_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17-runtime:1.14"; - private static final String JAVA_11_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11-runtime:1.14"; + private static final String JAVA_17_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17-runtime:1.15"; + private static final String JAVA_11_BASE_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11-runtime:1.15"; private static final String DEFAULT_BASE_IMAGE_USER = "185"; private static final String OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP = "io.opentelemetry.context.contextStorageProvider"; diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftConfig.java index 245c6bb941019..5739ded667a7c 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/OpenshiftConfig.java @@ -15,8 +15,8 @@ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) public class OpenshiftConfig { - public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11:1.14"; - public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.14"; + public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11:1.15"; + public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.15"; public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; @@ -46,9 +46,9 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17:1.14} + * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17:1.15} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.14} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.15} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java index b01459912145c..098725815e833 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java @@ -12,8 +12,8 @@ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) public class S2iConfig { - public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11:1.14"; - public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.14"; + public static final String DEFAULT_BASE_JVM_JDK11_IMAGE = "registry.access.redhat.com/ubi8/openjdk-11:1.15"; + public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.15"; public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; @@ -41,9 +41,9 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17:1.14} + * When the application is built against Java 17 or higher, {@code registry.access.redhat.com/ubi8/openjdk-17:1.15} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.14} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-11:1.15} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/flyway/deployment/src/test/java/db/migration/V1_0_1__Update.java b/extensions/flyway/deployment/src/test/java/db/migration/V1_0_1__Update.java new file mode 100644 index 0000000000000..38855b7bd4554 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/db/migration/V1_0_1__Update.java @@ -0,0 +1,18 @@ +package db.migration; + +import java.sql.Statement; + +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; + +/** + * Migration class for some testcases. + */ +public class V1_0_1__Update extends BaseJavaMigration { + @Override + public void migrate(Context context) throws Exception { + try (Statement statement = context.getConnection().createStatement()) { + statement.executeUpdate("INSERT INTO quarked_flyway VALUES (1001, 'test')"); + } + } +} diff --git a/extensions/flyway/deployment/src/test/java/db/migration/V1_0_2__Update.java b/extensions/flyway/deployment/src/test/java/db/migration/V1_0_2__Update.java new file mode 100644 index 0000000000000..3181a849404d5 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/db/migration/V1_0_2__Update.java @@ -0,0 +1,39 @@ +package db.migration; + +import java.sql.Statement; + +import org.flywaydb.core.api.MigrationVersion; +import org.flywaydb.core.api.migration.Context; +import org.flywaydb.core.api.migration.JavaMigration; + +/** + * Migration class for some testcases. + */ +public class V1_0_2__Update implements JavaMigration { + @Override + public MigrationVersion getVersion() { + return MigrationVersion.fromVersion("1.0.2"); + } + + @Override + public String getDescription() { + return getClass().getSimpleName(); + } + + @Override + public Integer getChecksum() { + return null; + } + + @Override + public boolean canExecuteInTransaction() { + return true; + } + + @Override + public void migrate(Context context) throws Exception { + try (Statement statement = context.getConnection().createStatement()) { + statement.executeUpdate("INSERT INTO quarked_flyway VALUES (1002, 'test')"); + } + } +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionCleanAndMigrateAtStartWithJavaMigrationTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionCleanAndMigrateAtStartWithJavaMigrationTest.java index 8b1f25d96663c..6c4acbfcf162c 100644 --- a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionCleanAndMigrateAtStartWithJavaMigrationTest.java +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionCleanAndMigrateAtStartWithJavaMigrationTest.java @@ -12,13 +12,14 @@ import org.flywaydb.core.Flyway; import org.flywaydb.core.api.MigrationVersion; -import org.flywaydb.core.api.migration.BaseJavaMigration; import org.flywaydb.core.api.migration.Context; import org.flywaydb.core.api.migration.JavaMigration; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import db.migration.V1_0_1__Update; +import db.migration.V1_0_2__Update; import io.agroal.api.AgroalDataSource; import io.quarkus.test.QuarkusUnitTest; @@ -33,7 +34,7 @@ public class FlywayExtensionCleanAndMigrateAtStartWithJavaMigrationTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addClasses(V1_0_1__Update.class, V1_0_2__Update.class) + .addClasses(V1_0_1__Update.class, V1_0_2__Update.class, V9_9_9__Update.class) .addAsResource("db/migration/V1.0.0__Quarkus.sql") .addAsResource("clean-and-migrate-at-start-config.properties", "application.properties")); @@ -53,19 +54,10 @@ public void testFlywayConfigInjection() throws SQLException { assertEquals("1.0.2", currentVersion, "Expected to be 1.0.2 as there is a SQL and two Java migration scripts"); } - public static class V1_0_1__Update extends BaseJavaMigration { - @Override - public void migrate(Context context) throws Exception { - try (Statement statement = context.getConnection().createStatement()) { - statement.executeUpdate("INSERT INTO quarked_flyway VALUES (1001, 'test')"); - } - } - } - - public static class V1_0_2__Update implements JavaMigration { + public static class V9_9_9__Update implements JavaMigration { @Override public MigrationVersion getVersion() { - return MigrationVersion.fromVersion("1.0.2"); + return MigrationVersion.fromVersion("9.9.9"); } @Override @@ -86,7 +78,7 @@ public boolean canExecuteInTransaction() { @Override public void migrate(Context context) throws Exception { try (Statement statement = context.getConnection().createStatement()) { - statement.executeUpdate("INSERT INTO quarked_flyway VALUES (1002, 'test')"); + statement.executeUpdate("INSERT INTO quarked_flyway VALUES (9999, 'should-not-be-added')"); } } } diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionFilesystemResourceTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionFilesystemResourceTest.java index 7d33b3491df1f..b0ba671cf0e98 100644 --- a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionFilesystemResourceTest.java +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionFilesystemResourceTest.java @@ -12,15 +12,13 @@ import jakarta.inject.Inject; import org.flywaydb.core.Flyway; -import org.flywaydb.core.api.MigrationVersion; -import org.flywaydb.core.api.migration.BaseJavaMigration; -import org.flywaydb.core.api.migration.Context; -import org.flywaydb.core.api.migration.JavaMigration; import org.h2.jdbc.JdbcSQLSyntaxErrorException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import db.migration.V1_0_1__Update; +import db.migration.V1_0_2__Update; import io.agroal.api.AgroalDataSource; import io.quarkus.test.QuarkusUnitTest; @@ -59,42 +57,4 @@ public void testFlywayConfigInjection() throws SQLException { assertEquals("1.0.3", currentVersion, "Expected to be 1.0.3 as there is a SQL and two Java migration scripts"); } - public static class V1_0_1__Update extends BaseJavaMigration { - @Override - public void migrate(Context context) throws Exception { - try (Statement statement = context.getConnection().createStatement()) { - statement.executeUpdate("INSERT INTO quarked_flyway VALUES (1001, 'test')"); - } - } - } - - public static class V1_0_2__Update implements JavaMigration { - @Override - public MigrationVersion getVersion() { - return MigrationVersion.fromVersion("1.0.2"); - } - - @Override - public String getDescription() { - return getClass().getSimpleName(); - } - - @Override - public Integer getChecksum() { - return null; - } - - @Override - public boolean canExecuteInTransaction() { - return true; - } - - @Override - public void migrate(Context context) throws Exception { - try (Statement statement = context.getConnection().createStatement()) { - statement.executeUpdate("INSERT INTO quarked_flyway VALUES (1002, 'test')"); - } - } - } - } diff --git a/extensions/flyway/deployment/src/test/resources/clean-and-migrate-at-start-config.properties b/extensions/flyway/deployment/src/test/resources/clean-and-migrate-at-start-config.properties index ef6fab20a9c65..26c07c53ca008 100644 --- a/extensions/flyway/deployment/src/test/resources/clean-and-migrate-at-start-config.properties +++ b/extensions/flyway/deployment/src/test/resources/clean-and-migrate-at-start-config.properties @@ -10,3 +10,4 @@ quarkus.flyway.table=test_flyway_history quarkus.flyway.baseline-on-migrate=false quarkus.flyway.baseline-version=0.0.1 quarkus.flyway.baseline-description=Initial description for test +quarkus.flyway.users.locations=db/migration \ No newline at end of file diff --git a/extensions/flyway/deployment/src/test/resources/clean-and-migrate-at-start-with-fs-resource-config.properties b/extensions/flyway/deployment/src/test/resources/clean-and-migrate-at-start-with-fs-resource-config.properties index 352ca17e521d9..65afb9365aa52 100644 --- a/extensions/flyway/deployment/src/test/resources/clean-and-migrate-at-start-with-fs-resource-config.properties +++ b/extensions/flyway/deployment/src/test/resources/clean-and-migrate-at-start-with-fs-resource-config.properties @@ -10,4 +10,4 @@ quarkus.flyway.table=test_flyway_history quarkus.flyway.baseline-on-migrate=false quarkus.flyway.baseline-version=0.0.1 quarkus.flyway.baseline-description=Initial description for test -quarkus.flyway.locations=filesystem:src/test/resources/db/migration +quarkus.flyway.locations=filesystem:src/test/resources/db/migration,classpath:db/migration diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/QuarkusPathLocationScanner.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/QuarkusPathLocationScanner.java index 509145744666b..e3df6ac28286f 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/QuarkusPathLocationScanner.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/QuarkusPathLocationScanner.java @@ -28,17 +28,20 @@ public final class QuarkusPathLocationScanner implements ResourceAndClassScanner private static Map> applicationCallbackClasses = Collections.emptyMap(); // the set default to aid unit tests private final Collection scannedResources; + private final Collection> scannedMigrationClasses; public QuarkusPathLocationScanner(Collection locations) { LOGGER.debugv("Locations: {0}", locations); this.scannedResources = new ArrayList<>(); + this.scannedMigrationClasses = new ArrayList<>(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); FileSystemScanner fileSystemScanner = null; for (String migrationFile : applicationMigrationFiles) { if (isClassPathResource(locations, migrationFile)) { LOGGER.debugf("Loading %s", migrationFile); + scannedResources.add(new ClassPathResource(null, migrationFile, classLoader, StandardCharsets.UTF_8)); } else if (migrationFile.startsWith(Location.FILESYSTEM_PREFIX)) { if (fileSystemScanner == null) { @@ -51,6 +54,13 @@ public QuarkusPathLocationScanner(Collection locations) { } } + // Filter the provided migration classes to match the provided locations. + for (Class migrationClass : applicationMigrationClasses) { + if (isClassPathResource(locations, migrationClass.getCanonicalName().replace('.', '/'))) { + LOGGER.debugf("Loading migration class %s", migrationClass.getCanonicalName()); + scannedMigrationClasses.add(migrationClass); + } + } } public static void setApplicationCallbackClasses(Map> callbackClasses) { @@ -96,7 +106,7 @@ private boolean isClassPathResource(Collection locations, String migra */ @Override public Collection> scanForClasses() { - return applicationMigrationClasses; + return scannedMigrationClasses; } public static void setApplicationMigrationFiles(Collection applicationMigrationFiles) { diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/registry/PreconfiguredServiceRegistryBuilder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/registry/PreconfiguredServiceRegistryBuilder.java index eef1d7cc1f433..0cd850422385d 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/registry/PreconfiguredServiceRegistryBuilder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/registry/PreconfiguredServiceRegistryBuilder.java @@ -17,6 +17,7 @@ import org.hibernate.engine.jdbc.cursor.internal.RefCursorSupportInitiator; import org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator; import org.hibernate.engine.jdbc.internal.JdbcServicesInitiator; +import org.hibernate.engine.jdbc.internal.SqlStatementLoggerInitiator; import org.hibernate.event.internal.EntityCopyObserverFactoryInitiator; import org.hibernate.integrator.spi.Integrator; import org.hibernate.persister.internal.PersisterClassResolverInitiator; @@ -234,6 +235,9 @@ private static List> buildQuarkusServiceInitiatorLis // Default implementation serviceInitiators.add(ParameterMarkerStrategyInitiator.INSTANCE); + // Default implementation + serviceInitiators.add(SqlStatementLoggerInitiator.INSTANCE); + serviceInitiators.trimToSize(); return serviceInitiators; } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/StandardHibernateORMInitiatorListProvider.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/StandardHibernateORMInitiatorListProvider.java index bb499897a92cb..7d92e318ad25e 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/StandardHibernateORMInitiatorListProvider.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/StandardHibernateORMInitiatorListProvider.java @@ -12,6 +12,7 @@ import org.hibernate.engine.jdbc.dialect.internal.DialectResolverInitiator; import org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator; import org.hibernate.engine.jdbc.internal.JdbcServicesInitiator; +import org.hibernate.engine.jdbc.internal.SqlStatementLoggerInitiator; import org.hibernate.event.internal.EntityCopyObserverFactoryInitiator; import org.hibernate.persister.internal.PersisterClassResolverInitiator; import org.hibernate.persister.internal.PersisterFactoryInitiator; @@ -34,7 +35,8 @@ * Hibernate ORM when running on Quarkus. * WARNING: this is a customized list: we started from a copy of ORM's standard * list, then changes have evolved. - * Also: Hibernate Reactive uses a different list. + * Also: Hibernate Reactive uses a different list, and there's an additional definition of + * services in PreconfiguredServiceRegistryBuilder. */ public final class StandardHibernateORMInitiatorListProvider implements InitialInitiatorListProvider { @@ -104,6 +106,9 @@ public List> initialInitiatorList() { // Default implementation serviceInitiators.add(ParameterMarkerStrategyInitiator.INSTANCE); + // Default implementation + serviceInitiators.add(SqlStatementLoggerInitiator.INSTANCE); + serviceInitiators.trimToSize(); return serviceInitiators; diff --git a/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/config/SettingsSpyingIdentifierGenerator.java b/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/config/SettingsSpyingIdentifierGenerator.java index f1d1f4fe70293..920269e71b9f5 100644 --- a/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/config/SettingsSpyingIdentifierGenerator.java +++ b/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/config/SettingsSpyingIdentifierGenerator.java @@ -23,7 +23,6 @@ public class SettingsSpyingIdentifierGenerator implements IdentifierGenerator { public static final List> collectedSettings = new ArrayList<>(); @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException { collectedSettings.add(new HashMap<>(serviceRegistry.getService(ConfigurationService.class).getSettings())); } diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/registry/PreconfiguredReactiveServiceRegistryBuilder.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/registry/PreconfiguredReactiveServiceRegistryBuilder.java index 5de1999a0e3b6..a5be0f364df0d 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/registry/PreconfiguredReactiveServiceRegistryBuilder.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/registry/PreconfiguredReactiveServiceRegistryBuilder.java @@ -16,6 +16,7 @@ import org.hibernate.engine.jdbc.connections.internal.MultiTenantConnectionProviderInitiator; import org.hibernate.engine.jdbc.cursor.internal.RefCursorSupportInitiator; import org.hibernate.engine.jdbc.internal.JdbcServicesInitiator; +import org.hibernate.engine.jdbc.internal.SqlStatementLoggerInitiator; import org.hibernate.event.internal.EntityCopyObserverFactoryInitiator; import org.hibernate.integrator.spi.Integrator; import org.hibernate.persister.internal.PersisterFactoryInitiator; @@ -241,6 +242,9 @@ private static List> buildQuarkusServiceInitiatorLis // Custom for Hibernate Reactive: ParameterMarkerStrategy serviceInitiators.add(NativeParametersHandling.INSTANCE); + // Default implementation + serviceInitiators.add(SqlStatementLoggerInitiator.INSTANCE); + serviceInitiators.trimToSize(); return serviceInitiators; } diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/registry/ReactiveHibernateInitiatorListProvider.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/registry/ReactiveHibernateInitiatorListProvider.java index 7334d7f96b905..fa57b9e8b3bde 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/registry/ReactiveHibernateInitiatorListProvider.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/registry/ReactiveHibernateInitiatorListProvider.java @@ -12,6 +12,7 @@ import org.hibernate.engine.jdbc.dialect.internal.DialectResolverInitiator; import org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator; import org.hibernate.engine.jdbc.internal.JdbcServicesInitiator; +import org.hibernate.engine.jdbc.internal.SqlStatementLoggerInitiator; import org.hibernate.event.internal.EntityCopyObserverFactoryInitiator; import org.hibernate.persister.internal.PersisterFactoryInitiator; import org.hibernate.property.access.internal.PropertyAccessStrategyResolverInitiator; @@ -123,6 +124,9 @@ public List> initialInitiatorList() { // Custom for Hibernate Reactive: ParameterMarkerStrategy serviceInitiators.add(NativeParametersHandling.INSTANCE); + // Default implementation + serviceInitiators.add(SqlStatementLoggerInitiator.INSTANCE); + serviceInitiators.trimToSize(); return serviceInitiators; } diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/QuarkusReactiveConnectionPoolInitiator.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/QuarkusReactiveConnectionPoolInitiator.java index 0aa11409ddabb..e1611e2bcbeb6 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/QuarkusReactiveConnectionPoolInitiator.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/QuarkusReactiveConnectionPoolInitiator.java @@ -3,13 +3,9 @@ import java.util.Map; import org.hibernate.boot.registry.StandardServiceInitiator; -import org.hibernate.dialect.Dialect; -import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.reactive.pool.ReactiveConnectionPool; import org.hibernate.reactive.pool.impl.ExternalSqlClientPool; -import org.hibernate.reactive.pool.impl.Parameters; import org.hibernate.service.spi.ServiceRegistryImplementor; import io.quarkus.hibernate.orm.runtime.migration.MultiTenancyStrategy; @@ -37,10 +33,8 @@ public ReactiveConnectionPool initiateService(Map configurationValues, ServiceRe // nothing to do, but given the separate hierarchies have to handle this here. return null; } - SqlStatementLogger sqlStatementLogger = registry.getService(JdbcServices.class).getSqlStatementLogger(); - final Dialect dialect = registry.getService(JdbcEnvironment.class).getDialect(); - Parameters parameters = Parameters.instance(dialect); - return new ExternalSqlClientPool(pool, sqlStatementLogger, parameters); + final JdbcServices jdbcService = registry.getService(JdbcServices.class); + return new ExternalSqlClientPool(pool, jdbcService.getSqlStatementLogger(), jdbcService.getSqlExceptionHelper()); } } diff --git a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUiProcessor.java b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUIProcessor.java similarity index 62% rename from extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUiProcessor.java rename to extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUIProcessor.java index a8c4e6362386f..8c1e87051ea77 100644 --- a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUiProcessor.java +++ b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUIProcessor.java @@ -2,34 +2,36 @@ import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.ExecutionTime; -import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.LaunchModeBuildItem; -import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.MenuPageBuildItem; import io.quarkus.devui.spi.page.Page; -import io.quarkus.info.runtime.InfoRecorder; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; /** * This processor is responsible for the dev ui widget. */ -public class InfoDevUiProcessor { +public class InfoDevUIProcessor { @BuildStep(onlyIf = IsDevelopment.class) - @Record(ExecutionTime.STATIC_INIT) - CardPageBuildItem create(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + MenuPageBuildItem create(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, InfoBuildTimeConfig config, ManagementInterfaceBuildTimeConfig managementInterfaceBuildTimeConfig, - LaunchModeBuildItem launchModeBuildItem, - InfoRecorder unused) { - CardPageBuildItem pageBuildItem = new CardPageBuildItem(); + LaunchModeBuildItem launchModeBuildItem) { + MenuPageBuildItem pageBuildItem = new MenuPageBuildItem(); var path = nonApplicationRootPathBuildItem.resolveManagementPath(config.path(), managementInterfaceBuildTimeConfig, launchModeBuildItem); - pageBuildItem.addPage(Page.externalPageBuilder("App Information") + + pageBuildItem.addBuildTimeData("infoUrl", path); + + pageBuildItem.addPage(Page.webComponentPageBuilder() + .title("Information") .icon("font-awesome-solid:circle-info") + .componentLink("qwc-info.js")); + pageBuildItem.addPage(Page.externalPageBuilder("Raw") .url(path) + .icon("font-awesome-solid:circle-info") .isJsonContent()); return pageBuildItem; diff --git a/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js new file mode 100644 index 0000000000000..3cd1f9e32c2d2 --- /dev/null +++ b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js @@ -0,0 +1,149 @@ +import { LitElement, html, css} from 'lit'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import { infoUrl } from 'build-time-data'; +import '@vaadin/progress-bar'; +import 'qui-card'; +import '@vaadin/icon'; + +/** + * This component shows the Info Screen + */ +export class QwcInfo extends LitElement { + + static styles = css` + :host { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(450px, 1fr)); + gap: 1em; + padding: 10px; + } + qui-card { + display: flex; + } + .cardContent { + display: flex; + align-items: center; + padding: 10px; + gap: 10px; + } + vaadin-icon { + font-size: xx-large; + } + `; + + static properties = { + _infoUrl: {state: false}, + _info: {state: true}, + }; + + constructor() { + super(); + this._infoUrl = infoUrl; + this._info = null; + } + + async connectedCallback() { + super.connectedCallback(); + await this.load(); + } + + async load() { + const response = await fetch(this._infoUrl) + const data = await response.json(); + this._info = data; + } + + render() { + if (this._info) { + return html` + ${this._renderOsInfo(this._info)} + ${this._renderJavaInfo(this._info)} + ${this._renderGitInfo(this._info)} + ${this._renderBuildInfo(this._info)} + `; + }else{ + return html` +
    +
    Fetching infomation...
    + +
    + `; + } + } + + _renderOsInfo(info){ + if(info.os){ + let os = info.os; + return html` +
    + ${this._renderOsIcon(os.name)} + + + + +
    Name${os.name}
    Version${os.version}
    Arch${os.arch}
    +
    +
    `; + } + } + + _renderJavaInfo(info){ + if(info.java){ + let java = info.java; + return html` +
    + + + +
    Version${java.version}
    +
    +
    `; + } + } + + _renderOsIcon(osname){ + + if(osname){ + if(osname.toLowerCase().startsWith("linux")){ + return html``; + }else if(osname.toLowerCase().startsWith("mac") || osname.toLowerCase().startsWith("darwin")){ + return html``; + }else if(osname.toLowerCase().startsWith("win")){ + return html``; + } + } + } + + _renderGitInfo(info){ + if(info.git){ + let git = info.git; + return html` +
    + + + + + +
    Branch${git.branch}
    Commit${git.commit.id}
    Time${git.commit.time}
    +
    +
    `; + } + } + + _renderBuildInfo(info){ + if(info.build){ + let build = info.build; + return html` +
    + + + + + +
    Group${build.group}
    Artifact${build.artifact}
    Version${build.version}
    Time${build.time}
    +
    +
    `; + } + } +} +customElements.define('qwc-info', QwcInfo); \ No newline at end of file diff --git a/extensions/kubernetes-service-binding/runtime/src/main/java/io/quarkus/kubernetes/service/binding/runtime/DatasourceServiceBindingConfigSourceFactory.java b/extensions/kubernetes-service-binding/runtime/src/main/java/io/quarkus/kubernetes/service/binding/runtime/DatasourceServiceBindingConfigSourceFactory.java index c12857249237c..c4ac6cf7c98e3 100644 --- a/extensions/kubernetes-service-binding/runtime/src/main/java/io/quarkus/kubernetes/service/binding/runtime/DatasourceServiceBindingConfigSourceFactory.java +++ b/extensions/kubernetes-service-binding/runtime/src/main/java/io/quarkus/kubernetes/service/binding/runtime/DatasourceServiceBindingConfigSourceFactory.java @@ -81,7 +81,7 @@ protected String formatUrl(String urlFormat, String type, String host, String da private boolean configureUrlPropertyUsingKey(Map properties, String key) { String value = serviceBinding.getProperties().get(key); - if (value == null) { + if (value == null || prefix == null) { return false; } else if (value.startsWith(prefix)) { properties.put(urlPropertyName, value); @@ -109,7 +109,7 @@ public Reactive() { } public Reactive(String urlFormat) { - super("reactive", "quarkus.datasource.reactive.url", "", urlFormat); + super("reactive", "quarkus.datasource.reactive.url", null, urlFormat); } } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddClusterRoleResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddClusterRoleResourceDecorator.java index d13091514834b..2074e4fd122f1 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddClusterRoleResourceDecorator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddClusterRoleResourceDecorator.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.Map; -import io.dekorate.kubernetes.decorator.Decorator; import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; import io.fabric8.kubernetes.api.model.KubernetesListBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; @@ -46,9 +45,4 @@ public void visit(KubernetesListBuilder list) { .endMetadata() .withRules(rules)); } - - @Override - public Class[] before() { - return new Class[] { AddRoleBindingResourceDecorator.class }; - } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java index 632b3bca7bcef..752efe7fd2b03 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddRoleResourceDecorator.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.Map; -import io.dekorate.kubernetes.decorator.Decorator; import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; import io.fabric8.kubernetes.api.model.KubernetesListBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; @@ -49,9 +48,4 @@ public void visit(KubernetesListBuilder list) { .endMetadata() .withRules(rules)); } - - @Override - public Class[] before() { - return new Class[] { AddRoleBindingResourceDecorator.class }; - } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceAccountResourceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceAccountResourceDecorator.java index f6f90801b3608..b8fb1f0eb8dc4 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceAccountResourceDecorator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddServiceAccountResourceDecorator.java @@ -5,7 +5,6 @@ import java.util.HashMap; import java.util.Map; -import io.dekorate.kubernetes.decorator.Decorator; import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; import io.fabric8.kubernetes.api.model.KubernetesListBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; @@ -44,9 +43,4 @@ public void visit(KubernetesListBuilder list) { .endMetadata() .endServiceAccountItem(); } - - @Override - public Class[] before() { - return new Class[] { AddRoleBindingResourceDecorator.class }; - } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java index cce484f5534c9..fa4d58dad9825 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java @@ -30,7 +30,6 @@ import io.dekorate.kubernetes.decorator.Decorator; import io.dekorate.logger.NoopLogger; import io.dekorate.processor.SimpleFileReader; -import io.dekorate.processor.SimpleFileWriter; import io.dekorate.project.Project; import io.dekorate.utils.Maps; import io.dekorate.utils.Strings; @@ -146,7 +145,7 @@ public void build(ApplicationInfoBuildItem applicationInfo, .map(DeploymentTargetEntry::getName) .collect(Collectors.toSet())); final Map generatedResourcesMap; - final SessionWriter sessionWriter = new SimpleFileWriter(project, false); + final SessionWriter sessionWriter = new QuarkusFileWriter(project); final SessionReader sessionReader = new SimpleFileReader( project.getRoot().resolve("src").resolve("main").resolve("kubernetes"), targets); sessionWriter.setProject(project); diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/QuarkusFileWriter.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/QuarkusFileWriter.java new file mode 100644 index 0000000000000..a837f359522dc --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/QuarkusFileWriter.java @@ -0,0 +1,105 @@ +package io.quarkus.kubernetes.deployment; + +import static io.quarkus.kubernetes.deployment.Constants.CLUSTER_ROLE; +import static io.quarkus.kubernetes.deployment.Constants.CLUSTER_ROLE_BINDING; +import static io.quarkus.kubernetes.deployment.Constants.CRONJOB; +import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT; +import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT_CONFIG; +import static io.quarkus.kubernetes.deployment.Constants.INGRESS; +import static io.quarkus.kubernetes.deployment.Constants.JOB; +import static io.quarkus.kubernetes.deployment.Constants.ROLE; +import static io.quarkus.kubernetes.deployment.Constants.ROLE_BINDING; +import static io.quarkus.kubernetes.deployment.Constants.ROUTE; +import static io.quarkus.kubernetes.deployment.Constants.SERVICE_ACCOUNT; +import static io.quarkus.kubernetes.deployment.Constants.STATEFULSET; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import io.dekorate.processor.SimpleFileWriter; +import io.dekorate.project.Project; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesList; +import io.fabric8.kubernetes.api.model.KubernetesListBuilder; + +public class QuarkusFileWriter extends SimpleFileWriter { + + private static final List RESOURCE_KIND_ORDER = List.of("Namespace", + "NetworkPolicy", + "ResourceQuota", + "LimitRange", + "PodSecurityPolicy", + "PodDisruptionBudget", + SERVICE_ACCOUNT, + "Secret", + "SecretList", + "ConfigMap", + "StorageClass", + "PersistentVolume", + "PersistentVolumeClaim", + "CustomResourceDefinition", + CLUSTER_ROLE, + "ClusterRoleList", + CLUSTER_ROLE_BINDING, + "ClusterRoleBindingList", + ROLE, + "RoleList", + ROLE_BINDING, + "RoleBindingList", + "Service", + "ImageStream", + "BuildConfig", + "DaemonSet", + "Pod", + "ReplicationController", + "ReplicaSet", + DEPLOYMENT, + "HorizontalPodAutoscaler", + STATEFULSET, + DEPLOYMENT_CONFIG, + JOB, + CRONJOB, + INGRESS, + ROUTE, + "APIService"); + + public QuarkusFileWriter(Project project) { + super(project, false); + } + + @Override + public Map write(String group, KubernetesList list) { + // sort resources in list by: ServiceAccount, Role, ClusterRole, the rest... + return super.write(group, new KubernetesListBuilder().addAllToItems(sort(list.getItems())).build()); + } + + private List sort(List items) { + // Resources that we need the order. + Map> groups = new HashMap<>(); + // List of resources with unknown order: we preserve the order of creation in this case + List rest = new LinkedList<>(); + for (HasMetadata item : items) { + String kind = item.getKind(); + if (RESOURCE_KIND_ORDER.contains(kind)) { + groups.computeIfAbsent(kind, k -> new LinkedList<>()) + .add(item); + } else { + rest.add(item); + } + } + + List sorted = new LinkedList<>(); + // we first add the resources with order + for (String kind : RESOURCE_KIND_ORDER) { + List group = groups.get(kind); + if (group != null) { + sorted.addAll(group); + } + } + + sorted.addAll(rest); + return sorted; + } +} diff --git a/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java index 4d5dc582cf76b..26efb7775171a 100644 --- a/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java +++ b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java @@ -27,6 +27,7 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; @@ -45,6 +46,7 @@ import io.quarkus.mailer.runtime.Mailers; import io.quarkus.mailer.runtime.MailersBuildTimeConfig; import io.quarkus.mailer.runtime.MailersRuntimeConfig; +import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.deployment.CheckedTemplateAdapterBuildItem; import io.quarkus.qute.deployment.QuteProcessor; import io.quarkus.qute.deployment.TemplatePathBuildItem; @@ -91,13 +93,15 @@ void registerBeans(BuildProducer beans) { @Record(ExecutionTime.STATIC_INIT) @BuildStep MailersBuildItem generateMailerSupportBean(MailerRecorder recorder, + CombinedIndexBuildItem index, BeanDiscoveryFinishedBuildItem beans, BuildProducer syntheticBeans) { List mailerInjectionPoints = beans.getInjectionPoints().stream() .filter(i -> SUPPORTED_INJECTION_TYPES.contains(i.getRequiredType().name())) .collect(Collectors.toList()); - boolean hasDefaultMailer = mailerInjectionPoints.stream().anyMatch(i -> i.hasDefaultedQualifier()); + boolean hasDefaultMailer = mailerInjectionPoints.stream().anyMatch(i -> i.hasDefaultedQualifier()) + || !index.getIndex().getAnnotations(CheckedTemplate.class).isEmpty(); Set namedMailers = mailerInjectionPoints.stream() .map(i -> i.getRequiredQualifier(MAILER_NAME)) diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java index 04fce11c4d0d3..39d66426f92d0 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java @@ -68,4 +68,6 @@ public final class OidcConstants { public static final String BACK_CHANNEL_LOGOUT_SID_CLAIM = "sid"; public static final String FRONT_CHANNEL_LOGOUT_SID_PARAM = "sid"; public static final String ID_TOKEN_SID_CLAIM = "sid"; + + public static final String OPENID_SCOPE = "openid"; } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/AbstractDevConsoleProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/AbstractDevConsoleProcessor.java index 99568273ad37f..2e6f427080efb 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/AbstractDevConsoleProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/AbstractDevConsoleProcessor.java @@ -18,6 +18,7 @@ public abstract class AbstractDevConsoleProcessor { protected static final String TOKEN_PATH_CONFIG_KEY = CONFIG_PREFIX + "token-path"; protected static final String END_SESSION_PATH_CONFIG_KEY = CONFIG_PREFIX + "end-session-path"; protected static final String POST_LOGOUT_URI_PARAM_CONFIG_KEY = CONFIG_PREFIX + "logout.post-logout-uri-param"; + protected static final String SCOPES_KEY = CONFIG_PREFIX + "authentication.scopes"; protected void produceDevConsoleTemplateItems(Capabilities capabilities, BuildProducer devConsoleTemplate, @@ -66,6 +67,9 @@ protected void produceDevConsoleTemplateItems(Capabilities capabilities, new DevConsoleRuntimeTemplateInfoBuildItem("postLogoutUriParam", new OidcConfigPropertySupplier(POST_LOGOUT_URI_PARAM_CONFIG_KEY), this.getClass(), curateOutcomeBuildItem)); + devConsoleRuntimeInfo.produce( + new DevConsoleRuntimeTemplateInfoBuildItem("scopes", + new OidcConfigPropertySupplier(SCOPES_KEY), this.getClass(), curateOutcomeBuildItem)); } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevConsoleProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevConsoleProcessor.java index c0679b768a2ac..84f96c0465768 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevConsoleProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevConsoleProcessor.java @@ -101,7 +101,8 @@ public void run() { metadata != null ? metadata.getString("authorization_endpoint") : null, metadata != null ? metadata.getString("token_endpoint") : null, metadata != null ? metadata.getString("end_session_endpoint") : null, - metadata != null ? metadata.containsKey("introspection_endpoint") : false); + metadata != null ? metadata.containsKey("introspection_endpoint") + || metadata.containsKey("userinfo_endpoint") : false); produceDevConsoleRouteItems(devConsoleRoute, new OidcTestServiceHandler(vertxInstance, oidcConfig.devui.webClientTimeout), diff --git a/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html b/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html index efd7382381617..0006e4572e603 100644 --- a/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html +++ b/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html @@ -10,7 +10,7 @@ var port = {config:property('quarkus.http.port')}; -{#if info:oidcApplicationType is 'service'} +{#if info:oidcApplicationType is 'service' || info:oidcApplicationType is 'hybrid'} var devRoot = '{devRootAppend}'; var encodedDevRoot = devRoot.replaceAll("/", "%2F"); @@ -40,7 +40,7 @@ $('.loginError').hide(); $('.implicitLoggedIn').show(); var search = window.location.search; - var code = search.match(/code=([^&]+)/)[1]; + var code = decodeURIComponent(search.match(/code=([^&]+)/)[1]); var state = search.match(/state=([^&]+)/)[1]; exchangeCodeForTokens(code, state); }else if(errorInUrl()){ @@ -103,6 +103,7 @@ var address; var state; var clientId = getClientId(); + var scopes = '{info:scopes??}'; {#if info:keycloakAdminUrl?? && info:keycloakRealms??} address = '{info:keycloakAdminUrl??}' + "/realms/" + $('#keycloakRealm').val() + "/protocol/openid-connect/auth"; state = makeid() + "_" + $('#keycloakRealm').val() + "_" + clientId; @@ -114,14 +115,14 @@ window.location.href = address + "?client_id=" + clientId + "&redirect_uri=" + "http%3A%2F%2Flocalhost%3A" + port + encodedDevRoot + "%2Fio.quarkus.quarkus-oidc%2Fprovider" - + "&scope=openid&response_type=token id_token&response_mode=query&prompt=login" + + "&scope=" + scopes + "&response_type=token id_token&response_mode=query&prompt=login" + "&nonce=" + makeid() + "&state=" + state; {#else} window.location.href = address + "?client_id=" + clientId + "&redirect_uri=" + "http%3A%2F%2Flocalhost%3A" + port + encodedDevRoot + "%2Fio.quarkus.quarkus-oidc%2Fprovider" - + "&scope=openid&response_type=code&response_mode=query&prompt=login" + + "&scope=" + scopes + "&response_type=code&response_mode=query&prompt=login" + "&nonce=" + makeid() + "&state=" + state; {/if} diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/OpaqueTokenVerificationWithUserInfoValidationTest.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/OpaqueTokenVerificationWithUserInfoValidationTest.java index b6eaff8715bec..1c53cc3965904 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/OpaqueTokenVerificationWithUserInfoValidationTest.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/OpaqueTokenVerificationWithUserInfoValidationTest.java @@ -16,7 +16,9 @@ public class OpaqueTokenVerificationWithUserInfoValidationTest { @RegisterExtension static final QuarkusUnitTest test = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addAsResource(new StringAsset("quarkus.oidc.token.verify-access-token-with-user-info=true\n"), + .addAsResource(new StringAsset( + "quarkus.oidc.token.verify-access-token-with-user-info=true\n" + + "quarkus.oidc.authentication.user-info-required=false\n"), "application.properties")) .assertException(t -> { Throwable e = t; diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java index d8e7361970eb7..6dc0e7d4a2ca5 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java @@ -29,24 +29,24 @@ public AbstractJsonObjectResponse(JsonObject json) { } public String getString(String name) { - return json.getString(name); + return contains(name) ? json.getString(name) : null; } public Boolean getBoolean(String name) { - return json.getBoolean(name); + return contains(name) ? json.getBoolean(name) : null; } public Long getLong(String name) { - JsonNumber number = json.getJsonNumber(name); + JsonNumber number = contains(name) ? json.getJsonNumber(name) : null; return number != null ? number.longValue() : null; } public JsonArray getArray(String name) { - return json.getJsonArray(name); + return contains(name) ? json.getJsonArray(name) : null; } public JsonObject getObject(String name) { - return json.getJsonObject(name); + return contains(name) ? json.getJsonObject(name) : null; } public JsonObject getJsonObject() { @@ -58,7 +58,7 @@ public Object get(String name) { } public boolean contains(String propertyName) { - return json.containsKey(propertyName); + return json.containsKey(propertyName) && !json.isNull(propertyName); } public Set getPropertyNames() { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index 0dde01e5b97c2..e76abffd709e7 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -533,7 +533,7 @@ && isRedirectFromProvider(context, configContext)) { : Collections.emptyList(); List scopes = new ArrayList<>(oidcConfigScopes.size() + 1); if (configContext.oidcConfig.getAuthentication().addOpenidScope.orElse(true)) { - scopes.add("openid"); + scopes.add(OidcConstants.OPENID_SCOPE); } scopes.addAll(oidcConfigScopes); codeFlowParams.append(AMP).append(OidcConstants.TOKEN_SCOPE).append(EQ) diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigPropertySupplier.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigPropertySupplier.java index 9220acadc32a6..bc7384b392e10 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigPropertySupplier.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfigPropertySupplier.java @@ -1,15 +1,18 @@ package io.quarkus.oidc.runtime; +import java.util.List; import java.util.Optional; import java.util.function.Supplier; import org.eclipse.microprofile.config.ConfigProvider; import io.quarkus.oidc.common.runtime.OidcCommonUtils; +import io.quarkus.oidc.common.runtime.OidcConstants; public class OidcConfigPropertySupplier implements Supplier { private static final String AUTH_SERVER_URL_CONFIG_KEY = "quarkus.oidc.auth-server-url"; private static final String END_SESSION_PATH_KEY = "quarkus.oidc.end-session-path"; + private static final String SCOPES_KEY = "quarkus.oidc.authentication.scopes"; private String oidcConfigProperty; private String defaultValue; private boolean urlProperty; @@ -40,6 +43,13 @@ public String get() { return checkUrlProperty(value); } return defaultValue; + } else if (SCOPES_KEY.equals(oidcConfigProperty)) { + Optional> scopes = ConfigProvider.getConfig().getOptionalValues(oidcConfigProperty, String.class); + if (scopes.isPresent()) { + return OidcCommonUtils.urlEncode(String.join(" ", scopes.get())); + } else { + return OidcConstants.OPENID_SCOPE; + } } else { return checkUrlProperty(ConfigProvider.getConfig().getOptionalValue(oidcConfigProperty, String.class)); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index 64187c10e2cef..f5fd7d6985710 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -172,6 +172,19 @@ private Uni createTenantContext(Vertx vertx, OidcTenantConf return Uni.createFrom().failure(t); } + if (oidcConfig.roles.source.orElse(null) == Source.userinfo && !enableUserInfo(oidcConfig)) { + throw new ConfigurationException( + "UserInfo is not required but UserInfo is expected to be the source of authorization roles"); + } + if (oidcConfig.token.verifyAccessTokenWithUserInfo && !enableUserInfo(oidcConfig)) { + throw new ConfigurationException( + "UserInfo is not required but 'verifyAccessTokenWithUserInfo' is enabled"); + } + if (!oidcConfig.authentication.isIdTokenRequired().orElse(true) && !enableUserInfo(oidcConfig)) { + throw new ConfigurationException( + "UserInfo is not required but it will be needed to verify a code flow access token"); + } + if (!oidcConfig.discoveryEnabled.orElse(true)) { if (!isServiceApp(oidcConfig)) { if (!oidcConfig.authorizationPath.isPresent() || !oidcConfig.tokenPath.isPresent()) { @@ -226,10 +239,6 @@ private Uni createTenantContext(Vertx vertx, OidcTenantConf } if (oidcConfig.token.verifyAccessTokenWithUserInfo) { - if (!oidcConfig.authentication.isUserInfoRequired().orElse(false)) { - throw new ConfigurationException( - "UserInfo is not required but 'verifyAccessTokenWithUserInfo' is enabled"); - } if (!oidcConfig.isDiscoveryEnabled().orElse(true)) { if (oidcConfig.userInfoPath.isEmpty()) { throw new ConfigurationException( @@ -251,6 +260,18 @@ public TenantConfigContext apply(OidcProvider p) { }); } + private static boolean enableUserInfo(OidcTenantConfig oidcConfig) { + Optional userInfoRequired = oidcConfig.authentication.isUserInfoRequired(); + if (userInfoRequired.isPresent()) { + if (!userInfoRequired.get()) { + return false; + } + } else { + oidcConfig.authentication.setUserInfoRequired(true); + } + return true; + } + private static TenantConfigContext createTenantContextFromPublicKey(OidcTenantConfig oidcConfig) { if (!isServiceApp(oidcConfig)) { throw new ConfigurationException("'public-key' property can only be used with the 'service' applications"); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index a9b245bf4e1da..88714eea23bdc 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -429,6 +429,9 @@ static OidcTenantConfig mergeTenantConfig(OidcTenantConfig tenant, OidcTenantCon if (tenant.token.issuer.isEmpty()) { tenant.token.issuer = provider.token.issuer; } + if (tenant.token.principalClaim.isEmpty()) { + tenant.token.principalClaim = provider.token.principalClaim; + } return tenant; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java index 6c94a35a37153..38909dfce8841 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java @@ -63,6 +63,7 @@ private static OidcTenantConfig google() { ret.setAuthServerUrl("https://accounts.google.com"); ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP); ret.getAuthentication().setScopes(List.of("openid", "email", "profile")); + ret.getToken().setPrincipalClaim("name"); return ret; } diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java index 37cc57d5294e9..e82bea9dd1368 100644 --- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java @@ -195,6 +195,7 @@ public void testAcceptGoogleProperties() throws Exception { assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); assertEquals("https://accounts.google.com", config.getAuthServerUrl().get()); + assertEquals("name", config.getToken().getPrincipalClaim().get()); assertEquals(List.of("openid", "email", "profile"), config.authentication.scopes.get()); } @@ -206,12 +207,14 @@ public void testOverrideGoogleProperties() throws Exception { tenant.setApplicationType(ApplicationType.HYBRID); tenant.setAuthServerUrl("http://localhost/wiremock"); tenant.authentication.setScopes(List.of("write")); + tenant.token.setPrincipalClaim("firstname"); OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GOOGLE)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals("firstname", config.getToken().getPrincipalClaim().get()); assertEquals(List.of("write"), config.authentication.scopes.get()); } diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/UserInfoTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/UserInfoTest.java new file mode 100644 index 0000000000000..dd28befc9a69f --- /dev/null +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/UserInfoTest.java @@ -0,0 +1,67 @@ +package io.quarkus.oidc.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; + +import org.junit.jupiter.api.Test; + +import io.quarkus.oidc.UserInfo; + +public class UserInfoTest { + UserInfo userInfo = new UserInfo( + "{" + + "\"name\": \"alice\"," + + "\"admin\": true," + + "\"email\": null," + + "\"id\": 1234," + + "\"permissions\": [\"read\", \"write\"]," + + "\"scopes\": {\"scope\": \"see\"}" + + "}"); + + @Test + public void testGetString() { + assertEquals("alice", userInfo.getString("name")); + assertNull(userInfo.getString("names")); + } + + @Test + public void testGetBoolean() { + assertTrue(userInfo.getBoolean("admin")); + assertNull(userInfo.getBoolean("admins")); + } + + @Test + public void testGetLong() { + assertEquals(1234, userInfo.getLong("id")); + assertNull(userInfo.getLong("ids")); + } + + @Test + public void testGetArray() { + JsonArray array = userInfo.getArray("permissions"); + assertNotNull(array); + assertEquals(2, array.size()); + assertEquals("read", array.getString(0)); + assertEquals("write", array.getString(1)); + assertNull(userInfo.getArray("permit")); + } + + @Test + public void testGetObject() { + JsonObject map = userInfo.getObject("scopes"); + assertNotNull(map); + assertEquals(1, map.size()); + assertEquals("see", map.getString("scope")); + assertNull(userInfo.getObject("scope")); + } + + @Test + public void testGetNullProperty() { + assertNull(userInfo.getString("email")); + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryLegacyConfigurationTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryLegacyConfigurationTest.java index 79cd04d59c38b..6ae06be35007c 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryLegacyConfigurationTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryLegacyConfigurationTest.java @@ -5,7 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.Arrays; +import java.util.List; import jakarta.inject.Inject; @@ -24,6 +24,7 @@ class OpenTelemetryLegacyConfigurationTest { .overrideConfigKey("quarkus.opentelemetry.enabled", "false") .overrideConfigKey("quarkus.opentelemetry.tracer.enabled", "false") .overrideConfigKey("quarkus.opentelemetry.propagators", "tracecontext") + .overrideConfigKey("quarkus.opentelemetry.tracer.resource-attributes", "service.name=authservice") .overrideConfigKey("quarkus.opentelemetry.tracer.suppress-non-application-uris", "false") .overrideConfigKey("quarkus.opentelemetry.tracer.include-static-resources", "true") .overrideConfigKey("quarkus.opentelemetry.tracer.sampler", "off") @@ -46,7 +47,9 @@ void config() { assertEquals(FALSE, oTelBuildConfig.enabled()); assertTrue(oTelBuildConfig.traces().enabled().isPresent()); assertEquals(FALSE, oTelBuildConfig.traces().enabled().get()); - assertEquals(Arrays.asList("tracecontext"), oTelBuildConfig.propagators()); // will not include the default baggagge + assertEquals(List.of("tracecontext"), oTelBuildConfig.propagators()); // will not include the default baggagge + assertTrue(oTelRuntimeConfig.resourceAttributes().isPresent()); + assertEquals("service.name=authservice", oTelRuntimeConfig.resourceAttributes().get().get(0)); assertEquals(FALSE, oTelRuntimeConfig.traces().suppressNonApplicationUris()); assertEquals(TRUE, oTelRuntimeConfig.traces().includeStaticResources()); assertEquals("always_off", oTelBuildConfig.traces().sampler()); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/OTelFallbackConfigSourceInterceptor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/OTelFallbackConfigSourceInterceptor.java index ce1ea16693839..9d97991d4ca18 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/OTelFallbackConfigSourceInterceptor.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/OTelFallbackConfigSourceInterceptor.java @@ -1,5 +1,6 @@ package io.quarkus.opentelemetry.runtime.config; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; @@ -15,21 +16,27 @@ @Priority(Priorities.LIBRARY + 300 + 5) public class OTelFallbackConfigSourceInterceptor extends FallbackConfigSourceInterceptor { + private final static Map FALLBACKS = new HashMap<>(); private final static LegacySamplerNameConverter LEGACY_SAMPLER_NAME_CONVERTER = new LegacySamplerNameConverter(); + static { + FALLBACKS.put("quarkus.otel.enabled", "quarkus.opentelemetry.enabled"); + FALLBACKS.put("quarkus.otel.traces.enabled", "quarkus.opentelemetry.tracer.enabled"); + FALLBACKS.put("quarkus.otel.propagators", "quarkus.opentelemetry.propagators"); + FALLBACKS.put("quarkus.otel.resource.attributes", "quarkus.opentelemetry.tracer.resource-attributes"); + FALLBACKS.put("quarkus.otel.traces.suppress-non-application-uris", + "quarkus.opentelemetry.tracer.suppress-non-application-uris"); + FALLBACKS.put("quarkus.otel.traces.include-static-resources", "quarkus.opentelemetry.tracer.include-static-resources"); + FALLBACKS.put("quarkus.otel.traces.sampler", "quarkus.opentelemetry.tracer.sampler"); + FALLBACKS.put("quarkus.otel.traces.sampler.arg", "quarkus.opentelemetry.tracer.sampler.ratio"); + FALLBACKS.put("quarkus.otel.exporter.otlp.enabled", "quarkus.opentelemetry.tracer.exporter.otlp.enabled"); + FALLBACKS.put("quarkus.otel.exporter.otlp.traces.legacy-endpoint", + "quarkus.opentelemetry.tracer.exporter.otlp.endpoint"); + FALLBACKS.put("quarkus.otel.exporter.otlp.traces.headers", "quarkus.opentelemetry.tracer.exporter.otlp.headers"); + } + public OTelFallbackConfigSourceInterceptor() { - super(Map.of( - "quarkus.otel.enabled", "quarkus.opentelemetry.enabled", - "quarkus.otel.traces.enabled", "quarkus.opentelemetry.tracer.enabled", - "quarkus.otel.propagators", "quarkus.opentelemetry.propagators", - "quarkus.otel.traces.suppress-non-application-uris", - "quarkus.opentelemetry.tracer.suppress-non-application-uris", - "quarkus.otel.traces.include-static-resources", "quarkus.opentelemetry.tracer.include-static-resources", - "quarkus.otel.traces.sampler", "quarkus.opentelemetry.tracer.sampler", - "quarkus.otel.traces.sampler.arg", "quarkus.opentelemetry.tracer.sampler.ratio", - "quarkus.otel.exporter.otlp.enabled", "quarkus.opentelemetry.tracer.exporter.otlp.enabled", - "quarkus.otel.exporter.otlp.traces.headers", "quarkus.opentelemetry.tracer.exporter.otlp.headers", - "quarkus.otel.exporter.otlp.traces.legacy-endpoint", "quarkus.opentelemetry.tracer.exporter.otlp.endpoint")); + super(FALLBACKS); } @Override @@ -44,10 +51,28 @@ public ConfigValue getValue(final ConfigSourceInterceptorContext context, final @Override public Iterator iterateNames(final ConfigSourceInterceptorContext context) { Set names = new HashSet<>(); - Iterator namesIterator = super.iterateNames(context); + Iterator namesIterator = context.iterateNames(); while (namesIterator.hasNext()) { - names.add(namesIterator.next()); + String name = namesIterator.next(); + String fallback = FALLBACKS.get(name); + // We only include the used property, so if it is a fallback (not mapped), it will be reported as unknown + if (fallback != null) { + ConfigValue nameValue = context.proceed(name); + ConfigValue fallbackValue = context.proceed(fallback); + if (nameValue == null) { + names.add(fallback); + } else if (fallbackValue == null) { + names.add(name); + } else if (nameValue.getConfigSourceOrdinal() >= fallbackValue.getConfigSourceOrdinal()) { + names.add(name); + } else { + names.add(fallback); + } + } else { + names.add(name); + } } + // TODO - Required because the defaults ConfigSource for mappings does not provide configuration names. names.add("quarkus.otel.enabled"); names.add("quarkus.otel.metrics.exporter"); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/OtelConfigRelocateConfigSourceInterceptor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/OtelConfigRelocateConfigSourceInterceptor.java deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java index 8cc1c50404282..e3f5b77ffcf00 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java @@ -360,6 +360,15 @@ public static Mutiny.Query bindParameters(Mutiny.Query query, Object[] par return query; } + public static Mutiny.SelectionQuery bindParameters(Mutiny.SelectionQuery query, Object[] params) { + if (params == null || params.length == 0) + return query; + for (int i = 0; i < params.length; i++) { + query.setParameter(i + 1, params[i]); + } + return query; + } + public static Mutiny.Query bindParameters(Mutiny.Query query, Map params) { if (params == null || params.size() == 0) return query; @@ -369,6 +378,15 @@ public static Mutiny.Query bindParameters(Mutiny.Query query, Map bindParameters(Mutiny.SelectionQuery query, Map params) { + if (params == null || params.size() == 0) + return query; + for (Entry entry : params.entrySet()) { + query.setParameter(entry.getKey(), entry.getValue()); + } + return query; + } + public static Uni executeUpdate(String query, Object... params) { return getSession().chain(session -> { Mutiny.Query jpaQuery = session.createQuery(query); diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/DevModeResource.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/DevModeResource.java index 2dd4221f96c86..9a7db850fdd0b 100644 --- a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/DevModeResource.java +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/DevModeResource.java @@ -11,6 +11,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import io.vertx.oracleclient.OracleException; import io.vertx.oracleclient.OraclePool; @Path("/dev") @@ -25,7 +26,7 @@ public class DevModeResource { public CompletionStage checkConnectionFailure() throws SQLException { CompletableFuture future = new CompletableFuture<>(); client.query("SELECT 1 FROM DUAL").execute(ar -> { - Class expectedExceptionClass = SQLException.class; + Class expectedExceptionClass = OracleException.class; if (ar.succeeded()) { future.complete(Response.serverError().entity("Expected SQL query to fail").build()); } else if (!expectedExceptionClass.isAssignableFrom(ar.cause().getClass())) { diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ReactiveOracleReloadTest.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ReactiveOracleReloadTest.java index b84018f654060..a953a81500034 100644 --- a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ReactiveOracleReloadTest.java +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ReactiveOracleReloadTest.java @@ -1,12 +1,14 @@ package io.quarkus.reactive.oracle.client; import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusDevModeTest; import io.restassured.RestAssured; +@Disabled("Failing on CI but working locally - must be investigated") public class ReactiveOracleReloadTest { @RegisterExtension diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/ClassRestClientContext.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/ClassRestClientContext.java index 1f2762d529d9f..b7d1f63436dde 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/ClassRestClientContext.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/ClassRestClientContext.java @@ -19,6 +19,7 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.jaxrs.client.reactive.runtime.ParameterAnnotationsSupplier; +import io.quarkus.jaxrs.client.reactive.runtime.ParameterDescriptorFromClassSupplier; import io.quarkus.jaxrs.client.reactive.runtime.ParameterGenericTypesSupplier; class ClassRestClientContext implements AutoCloseable { @@ -30,6 +31,9 @@ class ClassRestClientContext implements AutoCloseable { public final Map methodStaticFields = new HashMap<>(); public final Map methodParamAnnotationsStaticFields = new HashMap<>(); public final Map methodGenericParametersStaticFields = new HashMap<>(); + public final Map beanTypesParameterDescriptorsStaticFields = new HashMap<>(); + public final Map classesMap = new HashMap<>(); + private int beanParamIndex = 0; public ClassRestClientContext(String name, BuildProducer generatedClasses, String... interfaces) { @@ -53,12 +57,12 @@ public void close() { } protected FieldDescriptor createJavaMethodField(ClassInfo interfaceClass, MethodInfo method, int methodIndex) { - ResultHandle interfaceClassHandle = clinit.loadClassFromTCCL(interfaceClass.toString()); + ResultHandle interfaceClassHandle = loadClass(interfaceClass.toString()); ResultHandle parameterArray = clinit.newArray(Class.class, method.parametersCount()); for (int i = 0; i < method.parametersCount(); i++) { String parameterClass = method.parameterType(i).name().toString(); - clinit.writeArrayValue(parameterArray, i, clinit.loadClassFromTCCL(parameterClass)); + clinit.writeArrayValue(parameterArray, i, loadClass(parameterClass)); } ResultHandle javaMethodHandle = clinit.invokeVirtualMethod( @@ -125,4 +129,43 @@ protected Supplier getLazyJavaMethodGenericParametersField(int return javaMethodGenericParametersField; }; } + + /** + * Generates "Class.forName(beanClass)" to generate the parameter descriptors. This method will only be created if and only + * if the supplier is used in order to not have a penalty performance. + */ + protected Supplier getLazyBeanParameterDescriptors(String beanClass) { + return () -> { + FieldDescriptor field = beanTypesParameterDescriptorsStaticFields.get(beanClass); + if (field != null) { + return field; + } + + ResultHandle clazz = loadClass(beanClass); + + ResultHandle mapWithAnnotationsHandle = clinit.newInstance(MethodDescriptor.ofConstructor( + ParameterDescriptorFromClassSupplier.class, Class.class), + clazz); + field = FieldDescriptor.of(classCreator.getClassName(), "beanParamDescriptors" + beanParamIndex, Supplier.class); + classCreator.getFieldCreator(field).setModifiers(Modifier.FINAL | Modifier.STATIC); // needs to be package-private because it's used by subresources + clinit.writeStaticField(field, mapWithAnnotationsHandle); + + beanTypesParameterDescriptorsStaticFields.put(beanClass, field); + + beanParamIndex++; + + return field; + }; + } + + private ResultHandle loadClass(String className) { + ResultHandle classType = classesMap.get(className); + if (classType != null) { + return classType; + } + + ResultHandle classFromTCCL = clinit.loadClassFromTCCL(className); + classesMap.put(className, classFromTCCL); + return classFromTCCL; + } } diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 42fb8d05ef807..3113b9af635e7 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -19,6 +19,7 @@ import java.io.Closeable; import java.io.File; import java.io.InputStream; +import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; import java.nio.file.Path; import java.util.AbstractMap; @@ -152,6 +153,7 @@ import io.quarkus.gizmo.TryBlock; import io.quarkus.jaxrs.client.reactive.runtime.ClientResponseBuilderFactory; import io.quarkus.jaxrs.client.reactive.runtime.JaxrsClientReactiveRecorder; +import io.quarkus.jaxrs.client.reactive.runtime.ParameterDescriptorFromClassSupplier; import io.quarkus.jaxrs.client.reactive.runtime.RestClientBase; import io.quarkus.jaxrs.client.reactive.runtime.ToObjectArray; import io.quarkus.jaxrs.client.reactive.runtime.impl.MultipartResponseDataBase; @@ -313,7 +315,7 @@ public boolean test(Map anns) { }) .setResourceMethodCallback(new Consumer<>() { @Override - public void accept(EndpointIndexer.ResourceMethodCallbackData entry) { + public void accept(EndpointIndexer.ResourceMethodCallbackEntry entry) { MethodInfo method = entry.getMethodInfo(); String source = JaxrsClientReactiveProcessor.class.getSimpleName() + " > " + method.declaringClass() + "[" + method + "]"; @@ -909,9 +911,8 @@ A more full example of generated client (with sub-resource) can is at the bottom addQueryParam(jandexMethod, methodCreator, methodTarget, param.name, methodCreator.getMethodParam(paramIdx), jandexMethod.parameterType(paramIdx), index, methodCreator.getThis(), - methodCreator.readStaticField(methodGenericParametersField.get()), - methodCreator.readStaticField(methodParamAnnotationsField.get()), - paramIdx)); + getGenericTypeFromArray(methodCreator, methodGenericParametersField, paramIdx), + getAnnotationsFromArray(methodCreator, methodParamAnnotationsField, paramIdx))); } else if (param.parameterType == ParameterType.BEAN || param.parameterType == ParameterType.MULTI_PART_FORM) { // bean params require both, web-target and Invocation.Builder, modifications @@ -930,13 +931,17 @@ A more full example of generated client (with sub-resource) can is at the bottom AssignableResultHandle invocationBuilderRef = handleBeanParamMethod .createVariable(Invocation.Builder.class); handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); + + Supplier beanParamDescriptorsField = classContext + .getLazyBeanParameterDescriptors(beanParam.type); + formParams = addBeanParamData(jandexMethod, methodCreator, handleBeanParamMethod, - invocationBuilderRef, beanParam.getItems(), + invocationBuilderRef, classContext, beanParam.getItems(), methodCreator.getMethodParam(paramIdx), methodTarget, index, restClientInterface.getClassName(), methodCreator.getThis(), handleBeanParamMethod.getThis(), - formParams, methodGenericParametersField, methodParamAnnotationsField, paramIdx, multipart, + formParams, beanParamDescriptorsField, multipart, beanParam.type); handleBeanParamMethod.returnValue(invocationBuilderRef); @@ -945,9 +950,8 @@ A more full example of generated client (with sub-resource) can is at the bottom // methodTarget = methodTarget.resolveTemplate(paramname, paramvalue); addPathParam(methodCreator, methodTarget, param.name, methodCreator.getMethodParam(paramIdx), param.type, methodCreator.getThis(), - methodCreator.readStaticField(methodGenericParametersField.get()), - methodCreator.readStaticField(methodParamAnnotationsField.get()), - paramIdx); + getGenericTypeFromArray(methodCreator, methodGenericParametersField, paramIdx), + getAnnotationsFromArray(methodCreator, methodParamAnnotationsField, paramIdx)); } else if (param.parameterType == ParameterType.BODY) { // just store the index of parameter used to create the body, we'll use it later bodyParameterIdx = paramIdx; @@ -965,8 +969,9 @@ A more full example of generated client (with sub-resource) can is at the bottom handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, handleHeaderMethod.getMethodParam(1), param.type, - handleHeaderMethod.getThis(), methodGenericParametersField.get(), - methodParamAnnotationsField.get(), paramIdx); + handleHeaderMethod.getThis(), + getGenericTypeFromArray(handleHeaderMethod, methodGenericParametersField, paramIdx), + getAnnotationsFromArray(handleHeaderMethod, methodParamAnnotationsField, paramIdx)); handleHeaderMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleHeaderDescriptor, methodCreator.getMethodParam(paramIdx)); } else if (param.parameterType == ParameterType.COOKIE) { @@ -984,7 +989,8 @@ A more full example of generated client (with sub-resource) can is at the bottom addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, handleCookieMethod.getMethodParam(1), param.type, handleCookieMethod.getThis(), - methodGenericParametersField.get(), methodParamAnnotationsField.get(), paramIdx); + getGenericTypeFromArray(handleCookieMethod, methodGenericParametersField, paramIdx), + getAnnotationsFromArray(handleCookieMethod, methodParamAnnotationsField, paramIdx)); handleCookieMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleHeaderDescriptor, methodCreator.getMethodParam(paramIdx)); } else if (param.parameterType == ParameterType.FORM) { @@ -993,9 +999,9 @@ A more full example of generated client (with sub-resource) can is at the bottom addFormParam(methodCreator, param.name, methodCreator.getMethodParam(paramIdx), param.declaredType, param.signature, restClientInterface.getClassName(), methodCreator.getThis(), formParams, - methodCreator.readStaticField(methodGenericParametersField.get()), - methodCreator.readStaticField(methodParamAnnotationsField.get()), - paramIdx, multipart, + getGenericTypeFromArray(methodCreator, methodGenericParametersField, paramIdx), + getAnnotationsFromArray(methodCreator, methodParamAnnotationsField, paramIdx), + multipart, param.mimeType, param.partFileName, jandexMethod.declaringClass().name() + "." + jandexMethod.name()); } @@ -1215,9 +1221,8 @@ private void handleSubResourceMethod(List addPathParam(ownerMethod, webTarget, param.name, paramValue, param.type, client, - ownerMethod.readStaticField(methodGenericParametersField.get()), - ownerMethod.readStaticField(methodParamAnnotationsField.get()), - i); + getGenericTypeFromArray(ownerMethod, methodGenericParametersField, i), + getAnnotationsFromArray(ownerMethod, methodParamAnnotationsField, i)); } } @@ -1322,9 +1327,10 @@ private void handleSubResourceMethod(List addQueryParam(jandexMethod, subMethodCreator, methodTarget, param.name, paramValue, subParamField.type, index, subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(subParamField.genericsParametersField.get()), - subMethodCreator.readStaticField(subParamField.paramAnnotationsField.get()), - subParamField.paramIndex)); + getGenericTypeFromArray(subMethodCreator, subParamField.genericsParametersField, + subParamField.paramIndex), + getAnnotationsFromArray(subMethodCreator, subParamField.paramAnnotationsField, + subParamField.paramIndex))); } else if (param.parameterType == ParameterType.BEAN || param.parameterType == ParameterType.MULTI_PART_FORM) { // bean params require both, web-target and Invocation.Builder, modifications @@ -1341,17 +1347,20 @@ private void handleSubResourceMethod(List MethodCreator handleBeanParamMethod = subContext.classCreator.getMethodCreator( handleBeanParamDescriptor).setModifiers(Modifier.PRIVATE); + Supplier beanParamDescriptors = subContext + .getLazyBeanParameterDescriptors(beanParam.type); + AssignableResultHandle invocationBuilderRef = handleBeanParamMethod .createVariable(Invocation.Builder.class); handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, - invocationBuilderRef, beanParam.getItems(), + invocationBuilderRef, subContext, beanParam.getItems(), paramValue, methodTarget, index, interfaceClass.name().toString(), subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), formParams, - methodGenericParametersField, methodParamAnnotationsField, subParamField.paramIndex, + beanParamDescriptors, multipart, beanParam.type); handleBeanParamMethod.returnValue(invocationBuilderRef); @@ -1361,9 +1370,10 @@ private void handleSubResourceMethod(List addPathParam(subMethodCreator, methodTarget, param.name, paramValue, param.type, subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(subParamField.genericsParametersField.get()), - subMethodCreator.readStaticField(subParamField.paramAnnotationsField.get()), - subParamField.paramIndex); + getGenericTypeFromArray(subMethodCreator, subParamField.genericsParametersField, + subParamField.paramIndex), + getAnnotationsFromArray(subMethodCreator, subParamField.paramAnnotationsField, + subParamField.paramIndex)); } else if (param.parameterType == ParameterType.BODY) { // just store the index of parameter used to create the body, we'll use it later bodyParameterValue = paramValue; @@ -1384,9 +1394,10 @@ private void handleSubResourceMethod(List handleHeaderMethod.getMethodParam(1), param.type, handleHeaderMethod.readInstanceField(clientField, handleHeaderMethod.getThis()), - subParamField.genericsParametersField.get(), - subParamField.paramAnnotationsField.get(), - subParamField.paramIndex); + getGenericTypeFromArray(handleHeaderMethod, subParamField.genericsParametersField, + subParamField.paramIndex), + getAnnotationsFromArray(handleHeaderMethod, subParamField.paramAnnotationsField, + subParamField.paramIndex)); handleHeaderMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleHeaderDescriptor, paramValue); } else if (param.parameterType == ParameterType.COOKIE) { @@ -1406,9 +1417,10 @@ private void handleSubResourceMethod(List handleCookieMethod.getMethodParam(1), param.type, handleCookieMethod.readInstanceField(clientField, handleCookieMethod.getThis()), - subParamField.genericsParametersField.get(), - subParamField.paramAnnotationsField.get(), - subParamField.paramIndex); + getGenericTypeFromArray(handleCookieMethod, subParamField.genericsParametersField, + subParamField.paramIndex), + getAnnotationsFromArray(handleCookieMethod, subParamField.paramAnnotationsField, + subParamField.paramIndex)); handleCookieMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleCookieDescriptor, paramValue); } else if (param.parameterType == ParameterType.FORM) { @@ -1430,9 +1442,10 @@ private void handleSubResourceMethod(List subMethodCreator.getMethodParam(paramIdx), jandexSubMethod.parameterType(paramIdx), index, subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(subMethodGenericParametersField.get()), - subMethodCreator.readStaticField(subMethodParamAnnotationsField.get()), - paramIdx)); + getGenericTypeFromArray(subMethodCreator, subMethodGenericParametersField, + paramIdx), + getAnnotationsFromArray(subMethodCreator, subMethodParamAnnotationsField, + paramIdx))); } else if (param.parameterType == ParameterType.BEAN || param.parameterType == ParameterType.MULTI_PART_FORM) { // bean params require both, web-target and Invocation.Builder, modifications @@ -1445,20 +1458,23 @@ private void handleSubResourceMethod(List subMethod.getName() + "$$" + subMethodIndex + "$$handleBeanParam$$" + paramIdx, Invocation.Builder.class, Invocation.Builder.class, param.type); - MethodCreator handleBeanParamMethod = ownerContext.classCreator.getMethodCreator( + MethodCreator handleBeanParamMethod = subContext.classCreator.getMethodCreator( handleBeanParamDescriptor).setModifiers(Modifier.PRIVATE); + Supplier beanParamDescriptors = subContext + .getLazyBeanParameterDescriptors(beanParam.type); + AssignableResultHandle invocationBuilderRef = handleBeanParamMethod .createVariable(Invocation.Builder.class); handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, - invocationBuilderRef, beanParam.getItems(), + invocationBuilderRef, subContext, beanParam.getItems(), subMethodCreator.getMethodParam(paramIdx), methodTarget, index, interfaceClass.name().toString(), subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), formParams, - subMethodGenericParametersField, subMethodParamAnnotationsField, paramIdx, multipart, + beanParamDescriptors, multipart, beanParam.type); handleBeanParamMethod.returnValue(invocationBuilderRef); @@ -1468,9 +1484,8 @@ private void handleSubResourceMethod(List addPathParam(subMethodCreator, methodTarget, param.name, subMethodCreator.getMethodParam(paramIdx), param.type, subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(subMethodGenericParametersField.get()), - subMethodCreator.readStaticField(subMethodParamAnnotationsField.get()), - paramIdx); + getGenericTypeFromArray(subMethodCreator, subMethodGenericParametersField, paramIdx), + getAnnotationsFromArray(subMethodCreator, subMethodParamAnnotationsField, paramIdx)); } else if (param.parameterType == ParameterType.BODY) { // just store the index of parameter used to create the body, we'll use it later bodyParameterValue = subMethodCreator.getMethodParam(paramIdx); @@ -1489,7 +1504,8 @@ private void handleSubResourceMethod(List addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, handleHeaderMethod.getMethodParam(1), param.type, handleHeaderMethod.readInstanceField(clientField, handleHeaderMethod.getThis()), - subMethodGenericParametersField.get(), subMethodParamAnnotationsField.get(), paramIdx); + getGenericTypeFromArray(handleHeaderMethod, subMethodGenericParametersField, paramIdx), + getAnnotationsFromArray(handleHeaderMethod, subMethodParamAnnotationsField, paramIdx)); handleHeaderMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleHeaderDescriptor, subMethodCreator.getMethodParam(paramIdx)); } else if (param.parameterType == ParameterType.COOKIE) { @@ -1508,7 +1524,8 @@ private void handleSubResourceMethod(List handleCookieMethod.getMethodParam(1), param.type, handleCookieMethod.readInstanceField(clientField, handleCookieMethod.getThis()), - subMethodGenericParametersField.get(), subMethodParamAnnotationsField.get(), paramIdx); + getGenericTypeFromArray(handleCookieMethod, subMethodGenericParametersField, paramIdx), + getAnnotationsFromArray(handleCookieMethod, subMethodParamAnnotationsField, paramIdx)); handleCookieMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleCookieDescriptor, subMethodCreator.getMethodParam(paramIdx)); } else if (param.parameterType == ParameterType.FORM) { @@ -1635,7 +1652,7 @@ private void handleMultipartField(String formParamName, String partType, String String type, String parameterGenericType, ResultHandle fieldValue, AssignableResultHandle multipartForm, BytecodeCreator methodCreator, - ResultHandle client, String restClientInterfaceClassName, ResultHandle parameterAnnotations, int methodIndex, + ResultHandle client, String restClientInterfaceClassName, ResultHandle parameterAnnotations, ResultHandle genericType, String errorLocation) { BytecodeCreator ifValueNotNull = methodCreator.ifNotNull(fieldValue).trueBranch(); @@ -1679,7 +1696,7 @@ private void handleMultipartField(String formParamName, String partType, String } else { // go via converter ResultHandle convertedFormParam = convertParamToString(ifValueNotNull, client, fieldValue, type, genericType, - parameterAnnotations, methodIndex); + parameterAnnotations); BytecodeCreator parameterIsStringBranch = checkStringParam(ifValueNotNull, convertedFormParam, restClientInterfaceClassName, errorLocation); addString(parameterIsStringBranch, multipartForm, formParamName, null, partFilename, convertedFormParam); @@ -2270,6 +2287,7 @@ private AssignableResultHandle addBeanParamData(MethodInfo jandexMethod, // Invocation.Builder executePut$$enrichInvocationBuilder${noOfBeanParam}(Invocation.Builder) BytecodeCreator invocationBuilderEnricher, AssignableResultHandle invocationBuilder, + ClassRestClientContext classContext, List beanParamItems, ResultHandle param, // can only be used in the current method, not in `invocationBuilderEnricher` @@ -2280,18 +2298,17 @@ private AssignableResultHandle addBeanParamData(MethodInfo jandexMethod, ResultHandle invocationEnricherClient, // this client or containing client if this is a sub-client AssignableResultHandle formParams, - Supplier methodGenericTypeField, - Supplier methodParamAnnotationsField, - int paramIdx, boolean multipart, String beanParamClass) { + Supplier descriptorsField, + boolean multipart, String beanParamClass) { // Form params collector must be initialized at method root level before any inner blocks that may use it if (areFormParamsDefinedIn(beanParamItems)) { formParams = createFormDataIfAbsent(methodCreator, formParams, multipart); } - addSubBeanParamData(jandexMethod, methodCreator, invocationBuilderEnricher, invocationBuilder, beanParamItems, param, - target, + addSubBeanParamData(jandexMethod, methodCreator, invocationBuilderEnricher, invocationBuilder, classContext, + beanParamItems, param, target, index, restClientInterfaceClassName, client, invocationEnricherClient, formParams, - methodGenericTypeField, methodParamAnnotationsField, paramIdx, multipart, beanParamClass); + descriptorsField, multipart, beanParamClass); return formParams; } @@ -2300,6 +2317,7 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method // Invocation.Builder executePut$$enrichInvocationBuilder${noOfBeanParam}(Invocation.Builder) BytecodeCreator invocationBuilderEnricher, AssignableResultHandle invocationBuilder, + ClassRestClientContext classContext, List beanParamItems, ResultHandle param, // can only be used in the current method, not in `invocationBuilderEnricher` @@ -2310,9 +2328,8 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method // this client or containing client if this is a sub-client ResultHandle invocationEnricherClient, AssignableResultHandle formParams, - Supplier methodGenericTypeField, - Supplier methodParamAnnotationsField, - int paramIdx, boolean multipart, String beanParamClass) { + Supplier beanParamDescriptorField, + boolean multipart, String beanParamClass) { BytecodeCreator creator = methodCreator.ifNotNull(param).trueBranch(); BytecodeCreator invoEnricher = invocationBuilderEnricher.ifNotNull(invocationBuilderEnricher.getMethodParam(1)) .trueBranch(); @@ -2322,10 +2339,12 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method case BEAN_PARAM: BeanParamItem beanParamItem = (BeanParamItem) item; ResultHandle beanParamElementHandle = beanParamItem.extract(creator, param); - addSubBeanParamData(jandexMethod, creator, invoEnricher, invocationBuilder, beanParamItem.items(), - beanParamElementHandle, target, index, restClientInterfaceClassName, client, + Supplier newBeanParamDescriptorField = classContext + .getLazyBeanParameterDescriptors(beanParamItem.className()); + addSubBeanParamData(jandexMethod, creator, invoEnricher, invocationBuilder, classContext, + beanParamItem.items(), beanParamElementHandle, target, index, restClientInterfaceClassName, client, invocationEnricherClient, formParams, - methodGenericTypeField, methodParamAnnotationsField, paramIdx, multipart, + newBeanParamDescriptorField, multipart, beanParamItem.className()); break; case QUERY_PARAM: @@ -2335,9 +2354,8 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method queryParam.extract(creator, param), queryParam.getValueType(), index, client, - creator.readStaticField(methodGenericTypeField.get()), - creator.readStaticField(methodParamAnnotationsField.get()), - paramIdx)); + getGenericTypeFromParameter(creator, beanParamDescriptorField, item.fieldName()), + getAnnotationsFromParameter(creator, beanParamDescriptorField, item.fieldName()))); break; case COOKIE: CookieParamItem cookieParam = (CookieParamItem) item; @@ -2345,33 +2363,34 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method cookieParam.getCookieName(), cookieParam.extract(invoEnricher, invoEnricher.getMethodParam(1)), cookieParam.getParamType(), invocationEnricherClient, - methodGenericTypeField.get(), methodParamAnnotationsField.get(), paramIdx); + getGenericTypeFromParameter(invoEnricher, beanParamDescriptorField, item.fieldName()), + getAnnotationsFromParameter(invoEnricher, beanParamDescriptorField, item.fieldName())); break; case HEADER_PARAM: HeaderParamItem headerParam = (HeaderParamItem) item; addHeaderParam(invoEnricher, invocationBuilder, headerParam.getHeaderName(), headerParam.extract(invoEnricher, invoEnricher.getMethodParam(1)), - headerParam.getParamType(), invocationEnricherClient, methodGenericTypeField.get(), - methodParamAnnotationsField.get(), paramIdx); + headerParam.getParamType(), invocationEnricherClient, + getGenericTypeFromParameter(invoEnricher, beanParamDescriptorField, item.fieldName()), + getAnnotationsFromParameter(invoEnricher, beanParamDescriptorField, item.fieldName())); break; case PATH_PARAM: PathParamItem pathParam = (PathParamItem) item; addPathParam(creator, target, pathParam.getPathParamName(), pathParam.extract(creator, param), pathParam.getParamType(), client, - creator.readStaticField(methodGenericTypeField.get()), - creator.readStaticField(methodParamAnnotationsField.get()), - paramIdx); + getGenericTypeFromParameter(creator, beanParamDescriptorField, item.fieldName()), + getAnnotationsFromParameter(creator, beanParamDescriptorField, item.fieldName())); break; case FORM_PARAM: FormParamItem formParam = (FormParamItem) item; addFormParam(creator, formParam.getFormParamName(), formParam.extract(creator, param), formParam.getParamType(), formParam.getParamSignature(), restClientInterfaceClassName, client, formParams, - creator.readStaticField(methodGenericTypeField.get()), - creator.readStaticField(methodParamAnnotationsField.get()), - paramIdx, multipart, formParam.getMimeType(), formParam.getFileName(), + getGenericTypeFromParameter(creator, beanParamDescriptorField, item.fieldName()), + getAnnotationsFromParameter(creator, beanParamDescriptorField, item.fieldName()), + multipart, formParam.getMimeType(), formParam.getFileName(), beanParamClass + "." + formParam.getSourceName()); break; default: @@ -2380,6 +2399,64 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method } } + private ResultHandle getGenericTypeFromParameter(BytecodeCreator creator, Supplier supplier, + String name) { + // Will return Map + ResultHandle map = creator.invokeInterfaceMethod(ofMethod(Supplier.class, "get", Object.class), + creator.readStaticField(supplier.get())); + // Will return ParameterDescriptorFromClassSupplier.ParameterDescriptor; + ResultHandle value = creator.invokeInterfaceMethod(ofMethod(Map.class, "get", Object.class, Object.class), + map, creator.load(name)); + // if (value != null) return value.genericType; + AssignableResultHandle genericType = creator.createVariable(java.lang.reflect.Type.class); + BranchResult ifBranch = creator.ifNotNull(value); + BytecodeCreator ifNotNull = ifBranch.trueBranch(); + ifNotNull.assign(genericType, ifNotNull.readInstanceField( + FieldDescriptor.of(ParameterDescriptorFromClassSupplier.ParameterDescriptor.class, "genericType", + java.lang.reflect.Type.class), + value)); + // if (value == null) return null; + BytecodeCreator ifNull = ifBranch.falseBranch(); + ifNull.assign(genericType, ifNull.loadNull()); + return genericType; + } + + private ResultHandle getGenericTypeFromArray(BytecodeCreator creator, Supplier supplier, + int paramIdx) { + ResultHandle value = creator.invokeInterfaceMethod(ofMethod(Supplier.class, "get", Object.class), + creator.readStaticField(supplier.get())); + return creator.readArrayValue(creator.checkCast(value, java.lang.reflect.Type[].class), paramIdx); + } + + private ResultHandle getAnnotationsFromParameter(BytecodeCreator creator, Supplier supplier, + String name) { + // Will return Map + ResultHandle map = creator.invokeInterfaceMethod(ofMethod(Supplier.class, "get", Object.class), + creator.readStaticField(supplier.get())); + // Will return ParameterDescriptorFromClassSupplier.ParameterDescriptor; + ResultHandle value = creator.invokeInterfaceMethod(ofMethod(Map.class, "get", Object.class, Object.class), + map, creator.load(name)); + // if (value != null) return value.genericType; + AssignableResultHandle annotations = creator.createVariable(Annotation[].class); + BranchResult ifBranch = creator.ifNotNull(value); + BytecodeCreator ifNotNull = ifBranch.trueBranch(); + ifNotNull.assign(annotations, ifNotNull.readInstanceField( + FieldDescriptor.of(ParameterDescriptorFromClassSupplier.ParameterDescriptor.class, "annotations", + Annotation[].class), + value)); + // if (value == null) return null; + BytecodeCreator ifNull = ifBranch.falseBranch(); + ifNull.assign(annotations, ifNull.loadNull()); + return annotations; + } + + private ResultHandle getAnnotationsFromArray(BytecodeCreator creator, Supplier supplier, + int paramIdx) { + ResultHandle value = creator.invokeInterfaceMethod(ofMethod(Supplier.class, "get", Object.class), + creator.readStaticField(supplier.get())); + return creator.readArrayValue(creator.checkCast(value, Annotation[][].class), paramIdx); + } + private boolean areFormParamsDefinedIn(List beanParamItems) { for (Item item : beanParamItems) { switch (item.type()) { @@ -2406,7 +2483,7 @@ private ResultHandle addQueryParam(MethodInfo jandexMethod, BytecodeCreator meth // this client or containing client if we're in a subresource ResultHandle client, ResultHandle genericType, - ResultHandle paramAnnotations, int paramIndex) { + ResultHandle paramAnnotations) { AssignableResultHandle result = methodCreator.createVariable(WebTarget.class); BranchResult isParamNull = methodCreator.ifNull(queryParamHandle); @@ -2453,7 +2530,7 @@ private ResultHandle addQueryParam(MethodInfo jandexMethod, BytecodeCreator meth } // get the new WebTarget addQueryParamToWebTarget(loopCreator, key, result, client, genericType, paramAnnotations, - paramIndex, paramArray, componentType, result); + paramArray, componentType, result); } else { ResultHandle paramArray; String componentType = null; @@ -2481,8 +2558,7 @@ private ResultHandle addQueryParam(MethodInfo jandexMethod, BytecodeCreator meth } addQueryParamToWebTarget(notNullParam, notNullParam.load(paramName), webTarget, client, genericType, - paramAnnotations, paramIndex, - paramArray, componentType, result); + paramAnnotations, paramArray, componentType, result); } isParamNull.trueBranch().assign(result, webTarget); @@ -2521,14 +2597,13 @@ private BranchResult iteratorHasNext(BytecodeCreator creator, ResultHandle itera private void addQueryParamToWebTarget(BytecodeCreator creator, ResultHandle paramName, ResultHandle webTarget, ResultHandle client, ResultHandle genericType, - ResultHandle paramAnnotations, int paramIndex, ResultHandle paramArray, + ResultHandle paramAnnotations, ResultHandle paramArray, String componentType, AssignableResultHandle resultVariable) { ResultHandle convertedParamArray = creator.invokeVirtualMethod( MethodDescriptor.ofMethod(RestClientBase.class, "convertParamArray", Object[].class, Object[].class, - Class.class, Supplier.class, Supplier.class, int.class), - client, paramArray, creator.loadClassFromTCCL(componentType), genericType, paramAnnotations, - creator.load(paramIndex)); + Class.class, java.lang.reflect.Type.class, Annotation[].class), + client, paramArray, creator.loadClassFromTCCL(componentType), genericType, paramAnnotations); creator.assign(resultVariable, creator.invokeInterfaceMethod( MethodDescriptor.ofMethod(WebTarget.class, "queryParam", WebTarget.class, @@ -2563,19 +2638,15 @@ private boolean isMap(Type type, IndexView index) { private void addHeaderParam(BytecodeCreator invoBuilderEnricher, AssignableResultHandle invocationBuilder, String paramName, ResultHandle headerParamHandle, String paramType, ResultHandle client, - FieldDescriptor methodGenericTypeField, FieldDescriptor methodParamAnnotationsField, - int paramIdx) { + ResultHandle genericType, ResultHandle annotations) { BytecodeCreator notNullValue = invoBuilderEnricher.ifNull(headerParamHandle).falseBranch(); - ResultHandle genericType = notNullValue.readStaticField(methodGenericTypeField); - - ResultHandle parameterAnnotations = notNullValue.readStaticField(methodParamAnnotationsField); headerParamHandle = notNullValue.invokeVirtualMethod( MethodDescriptor.ofMethod(RestClientBase.class, "convertParam", Object.class, - Object.class, Class.class, Supplier.class, Supplier.class, int.class), + Object.class, Class.class, java.lang.reflect.Type.class, Annotation[].class), client, headerParamHandle, - notNullValue.loadClassFromTCCL(paramType), genericType, parameterAnnotations, notNullValue.load(paramIdx)); + notNullValue.loadClassFromTCCL(paramType), genericType, annotations); notNullValue.assign(invocationBuilder, notNullValue.invokeInterfaceMethod( @@ -2586,13 +2657,12 @@ private void addHeaderParam(BytecodeCreator invoBuilderEnricher, AssignableResul private void addPathParam(BytecodeCreator methodCreator, AssignableResultHandle methodTarget, String paramName, ResultHandle pathParamHandle, String parameterType, ResultHandle client, - ResultHandle genericType, ResultHandle parameterAnnotations, int paramIndex) { + ResultHandle genericType, ResultHandle parameterAnnotations) { ResultHandle handle = methodCreator.invokeVirtualMethod( MethodDescriptor.ofMethod(RestClientBase.class, "convertParam", Object.class, - Object.class, Class.class, Supplier.class, Supplier.class, int.class), + Object.class, Class.class, java.lang.reflect.Type.class, Annotation[].class), client, pathParamHandle, - methodCreator.loadClassFromTCCL(parameterType), genericType, parameterAnnotations, - methodCreator.load(paramIndex)); + methodCreator.loadClassFromTCCL(parameterType), genericType, parameterAnnotations); methodCreator.assign(methodTarget, methodCreator.invokeInterfaceMethod(WEB_TARGET_RESOLVE_TEMPLATE_METHOD, methodTarget, @@ -2603,17 +2673,17 @@ private void addFormParam(BytecodeCreator methodCreator, String paramName, Resul String parameterType, String parameterGenericType, String restClientInterfaceClassName, ResultHandle client, AssignableResultHandle formParams, ResultHandle genericType, - ResultHandle parameterAnnotations, int methodIndex, boolean multipart, + ResultHandle parameterAnnotations, boolean multipart, String partType, String partFilename, String errorLocation) { if (multipart) { handleMultipartField(paramName, partType, partFilename, parameterType, parameterGenericType, formParamHandle, formParams, methodCreator, - client, restClientInterfaceClassName, parameterAnnotations, methodIndex, genericType, + client, restClientInterfaceClassName, parameterAnnotations, genericType, errorLocation); } else { BytecodeCreator notNullValue = methodCreator.ifNull(formParamHandle).falseBranch(); ResultHandle convertedFormParam = convertParamToString(notNullValue, client, formParamHandle, parameterType, - genericType, parameterAnnotations, methodIndex); + genericType, parameterAnnotations); BytecodeCreator parameterIsStringBranch = checkStringParam(notNullValue, convertedFormParam, restClientInterfaceClassName, errorLocation); parameterIsStringBranch.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, @@ -2637,30 +2707,25 @@ private BytecodeCreator checkStringParam(BytecodeCreator notNullValue, ResultHan private ResultHandle convertParamToString(BytecodeCreator notNullValue, ResultHandle client, ResultHandle formParamHandle, String parameterType, - ResultHandle genericType, ResultHandle parameterAnnotations, int methodIndex) { + ResultHandle genericType, ResultHandle parameterAnnotations) { return notNullValue.invokeVirtualMethod( MethodDescriptor.ofMethod(RestClientBase.class, "convertParam", Object.class, - Object.class, Class.class, Supplier.class, Supplier.class, int.class), + Object.class, Class.class, java.lang.reflect.Type.class, Annotation[].class), client, formParamHandle, - notNullValue.loadClassFromTCCL(parameterType), genericType, parameterAnnotations, - notNullValue.load(methodIndex)); + notNullValue.loadClassFromTCCL(parameterType), genericType, parameterAnnotations); } private void addCookieParam(BytecodeCreator invoBuilderEnricher, AssignableResultHandle invocationBuilder, String paramName, ResultHandle cookieParamHandle, String paramType, ResultHandle client, - FieldDescriptor methodGenericTypeField, FieldDescriptor methodParamAnnotationsField, int paramIdx) { + ResultHandle genericType, ResultHandle annotations) { BytecodeCreator notNullValue = invoBuilderEnricher.ifNull(cookieParamHandle).falseBranch(); - ResultHandle genericType = notNullValue.readStaticField(methodGenericTypeField); - - ResultHandle parameterAnnotations = notNullValue.readStaticField(methodParamAnnotationsField); - cookieParamHandle = notNullValue.invokeVirtualMethod( MethodDescriptor.ofMethod(RestClientBase.class, "convertParam", Object.class, - Object.class, Class.class, Supplier.class, Supplier.class, int.class), + Object.class, Class.class, java.lang.reflect.Type.class, Annotation[].class), client, cookieParamHandle, - notNullValue.loadClassFromTCCL(paramType), genericType, parameterAnnotations, notNullValue.load(paramIdx)); + notNullValue.loadClassFromTCCL(paramType), genericType, annotations); notNullValue.assign(invocationBuilder, notNullValue.invokeInterfaceMethod( MethodDescriptor.ofMethod(Invocation.Builder.class, "cookie", Invocation.Builder.class, String.class, diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/ParameterDescriptorFromClassSupplier.java b/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/ParameterDescriptorFromClassSupplier.java new file mode 100644 index 0000000000000..72cb181e8f295 --- /dev/null +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/ParameterDescriptorFromClassSupplier.java @@ -0,0 +1,52 @@ +package io.quarkus.jaxrs.client.reactive.runtime; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class ParameterDescriptorFromClassSupplier + implements Supplier> { + + private final Class clazz; + private volatile Map value; + + public ParameterDescriptorFromClassSupplier(Class clazz) { + this.clazz = clazz; + } + + @Override + public Map get() { + if (value == null) { + value = new HashMap<>(); + Class currentClass = clazz; + while (currentClass != null && currentClass != Object.class) { + for (Field field : currentClass.getDeclaredFields()) { + ParameterDescriptor descriptor = new ParameterDescriptor(); + descriptor.annotations = field.getAnnotations(); + descriptor.genericType = field.getGenericType(); + value.put(field.getName(), descriptor); + } + + for (Method method : currentClass.getDeclaredMethods()) { + ParameterDescriptor descriptor = new ParameterDescriptor(); + descriptor.annotations = method.getAnnotations(); + descriptor.genericType = method.getGenericReturnType(); + value.put(method.getName(), descriptor); + } + + currentClass = currentClass.getSuperclass(); + } + } + + return value; + } + + public static class ParameterDescriptor { + public Annotation[] annotations; + public Type genericType; + } +} diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/RestClientBase.java b/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/RestClientBase.java index c7f70ad6d0854..45c8aef084b2c 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/RestClientBase.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/RestClientBase.java @@ -7,7 +7,6 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; import jakarta.ws.rs.ext.ParamConverter; import jakarta.ws.rs.ext.ParamConverterProvider; @@ -122,9 +121,8 @@ public RestClientBase(List providers) { } @SuppressWarnings("unused") // used by generated code - public Object[] convertParamArray(T[] value, Class type, Supplier genericType, - Supplier methodAnnotations, int paramIndex) { - ParamConverter converter = getConverter(type, genericType, methodAnnotations, paramIndex); + public Object[] convertParamArray(T[] value, Class type, Type genericType, Annotation[] annotations) { + ParamConverter converter = getConverter(type, genericType, annotations); if (converter == null) { return value; @@ -139,10 +137,8 @@ public Object[] convertParamArray(T[] value, Class type, Supplier } @SuppressWarnings("unused") // used by generated code - public Object convertParam(T value, Class type, Supplier genericType, - Supplier methodAnnotations, - int paramIndex) { - ParamConverter converter = getConverter(type, genericType, methodAnnotations, paramIndex); + public Object convertParam(T value, Class type, Type genericType, Annotation[] annotations) { + ParamConverter converter = getConverter(type, genericType, annotations); if (converter != null) { return converter.toString(value); } else { @@ -154,30 +150,26 @@ public Object convertParam(T value, Class type, Supplier genericT } } - private ParamConverter getConverter(Class type, Supplier genericType, - Supplier methodAnnotations, - int paramIndex) { + private ParamConverter getConverter(Class type, Type genericType, Annotation[] annotations) { ParamConverterProvider converterProvider = providerForClass.get(type); if (converterProvider == null) { for (ParamConverterProvider provider : paramConverterProviders) { - ParamConverter converter = provider.getConverter(type, genericType.get()[paramIndex], - methodAnnotations.get()[paramIndex]); + ParamConverter converter = provider.getConverter(type, genericType, annotations); if (converter != null) { providerForClass.put(type, provider); return converter; } } // FIXME: this should go in favour of generating them, so we can generate them only if used for dead-code elimination - ParamConverter converter = DEFAULT_PROVIDER.getConverter(type, genericType.get()[paramIndex], - methodAnnotations.get()[paramIndex]); + ParamConverter converter = DEFAULT_PROVIDER.getConverter(type, genericType, annotations); if (converter != null) { providerForClass.put(type, DEFAULT_PROVIDER); return converter; } providerForClass.put(type, NO_PROVIDER); } else if (converterProvider != NO_PROVIDER) { - return converterProvider.getConverter(type, genericType.get()[paramIndex], methodAnnotations.get()[paramIndex]); + return converterProvider.getConverter(type, genericType, annotations); } return null; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusServerEndpointIndexer.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusServerEndpointIndexer.java index 62181bcb5d575..b7cfc7052729f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusServerEndpointIndexer.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusServerEndpointIndexer.java @@ -1,5 +1,8 @@ package io.quarkus.resteasy.reactive.server.deployment; +import static org.jboss.resteasy.reactive.server.processor.util.ResteasyReactiveServerDotNames.SERVER_MESSAGE_BODY_READER; + +import java.lang.annotation.Annotation; import java.util.List; import java.util.Map; import java.util.function.Predicate; @@ -14,6 +17,8 @@ import org.jboss.jandex.Type; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.common.ResteasyReactiveConfig; +import org.jboss.resteasy.reactive.common.model.MethodParameter; +import org.jboss.resteasy.reactive.common.model.ParameterType; import org.jboss.resteasy.reactive.common.processor.DefaultProducesHandler; import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveScanner; import org.jboss.resteasy.reactive.common.processor.scanning.ScannedSerializer; @@ -24,9 +29,11 @@ import org.jboss.resteasy.reactive.server.processor.ServerIndexedParameter; import org.jboss.resteasy.reactive.server.spi.EndpointInvokerFactory; +import io.quarkus.builder.BuildException; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.deployment.util.JandexUtil; import io.quarkus.resteasy.reactive.common.deployment.JsonDefaultProducersHandler; import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveRecorder; @@ -147,16 +154,73 @@ protected void handleAdditionalMethodProcessing(ServerResourceMethod method, Cla warnAboutMissingJsonProviderIfNeeded(method, info); } + @Override + public boolean additionalRegisterClassForReflectionCheck(ResourceMethodCallbackEntry entry) { + return checkBodyParameterMessageBodyReader(entry); + } + + /** + * Check whether the Resource Method has a body parameter for which there exists a matching + * {@link jakarta.ws.rs.ext.MessageBodyReader} + * that is not a {@link org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader}. + * In this case the Resource Class needs to be registered for reflection because the + * {@link jakarta.ws.rs.ext.MessageBodyReader#isReadable(Class, java.lang.reflect.Type, Annotation[], MediaType)} + * method expects to be passed the method annotations. + */ + private boolean checkBodyParameterMessageBodyReader(ResourceMethodCallbackEntry entry) { + MethodParameter[] parameters = entry.getResourceMethod().getParameters(); + if (parameters.length == 0) { + return false; + } + MethodParameter bodyParameter = null; + for (MethodParameter parameter : parameters) { + if (parameter.parameterType == ParameterType.BODY) { + bodyParameter = parameter; + break; + } + } + if (bodyParameter == null) { + return false; + } + String parameterClassName = bodyParameter.getDeclaredType(); + List readers = getSerializerScanningResult().getReaders(); + + for (ScannedSerializer reader : readers) { + if (isSubclassOf(parameterClassName, reader.getHandledClassName()) && !isServerMessageBodyReader( + reader.getClassInfo())) { + return true; + } + } + return false; + } + + private boolean isSubclassOf(String className, String parentName) { + if (className.equals(parentName)) { + return true; + } + ClassInfo classByName = index.getClassByName(className); + if (classByName == null) { + return false; + } + try { + return JandexUtil.isSubclassOf(index, classByName, + DotName.createSimple(parentName)); + } catch (BuildException e) { + return false; + } + } + + private boolean isServerMessageBodyReader(ClassInfo readerClassInfo) { + return index.getAllKnownImplementors(SERVER_MESSAGE_BODY_READER).contains(readerClassInfo); + } + private void warnAboutMissingJsonProviderIfNeeded(ServerResourceMethod method, MethodInfo info) { if (!capabilities.isCapabilityWithPrefixMissing("io.quarkus.resteasy.reactive.json")) { return; } if (hasJson(method) || (hasNoTypesDefined(method) && isDefaultJson())) { - if (serializerScanningResult == null) { - serializerScanningResult = ResteasyReactiveScanner.scanForSerializers(index, applicationScanningResult); - } - boolean appProvidedJsonReaderExists = appProvidedJsonProviderExists(serializerScanningResult.getReaders()); - boolean appProvidedJsonWriterExists = appProvidedJsonProviderExists(serializerScanningResult.getWriters()); + boolean appProvidedJsonReaderExists = appProvidedJsonProviderExists(getSerializerScanningResult().getReaders()); + boolean appProvidedJsonWriterExists = appProvidedJsonProviderExists(getSerializerScanningResult().getWriters()); if (!appProvidedJsonReaderExists || !appProvidedJsonWriterExists) { LOGGER.warnf("Quarkus detected the use of JSON in JAX-RS method '" + info.declaringClass().name() + "#" + info.name() @@ -165,6 +229,13 @@ private void warnAboutMissingJsonProviderIfNeeded(ServerResourceMethod method, M } } + private SerializerScanningResult getSerializerScanningResult() { + if (serializerScanningResult == null) { + serializerScanningResult = ResteasyReactiveScanner.scanForSerializers(index, applicationScanningResult); + } + return serializerScanningResult; + } + private boolean appProvidedJsonProviderExists(List providers) { boolean appProvidedJsonReaderExists = false; for (ScannedSerializer provider : providers) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 8f582abc8e2bd..7d4574abb0d99 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -498,7 +498,7 @@ public void setupEndpoints(ApplicationIndexBuildItem applicationIndexBuildItem, : Collections.emptyMap()) .setResourceMethodCallback(new Consumer<>() { @Override - public void accept(EndpointIndexer.ResourceMethodCallbackData entry) { + public void accept(EndpointIndexer.ResourceMethodCallbackEntry entry) { MethodInfo method = entry.getMethodInfo(); resourceMethodEntries.add(new ResteasyReactiveResourceMethodEntriesBuildItem.Entry( @@ -545,18 +545,27 @@ public void accept(EndpointIndexer.ResourceMethodCallbackData entry) { .build()); } if (parameterType.name().equals(FILE)) { - reflectiveClassBuildItemBuildProducer - .produce(ReflectiveClassBuildItem - .builder(entry.getActualEndpointInfo().name().toString()) - .constructors(false).methods().build()); + minimallyRegisterResourceClassForReflection(entry, + reflectiveClassBuildItemBuildProducer); } } if (filtersAccessResourceMethod) { - reflectiveClassBuildItemBuildProducer.produce( - ReflectiveClassBuildItem.builder(entry.getActualEndpointInfo().name().toString()) - .constructors(false).methods().build()); + minimallyRegisterResourceClassForReflection(entry, reflectiveClassBuildItemBuildProducer); } + + if (entry.additionalRegisterClassForReflectionCheck()) { + minimallyRegisterResourceClassForReflection(entry, reflectiveClassBuildItemBuildProducer); + } + } + + private void minimallyRegisterResourceClassForReflection( + EndpointIndexer.ResourceMethodCallbackEntry entry, + BuildProducer reflectiveClassBuildItemBuildProducer) { + reflectiveClassBuildItemBuildProducer + .produce(ReflectiveClassBuildItem + .builder(entry.getActualEndpointInfo().name().toString()) + .constructors(false).methods().build()); } private boolean hasAnnotation(MethodInfo method, short paramPosition, DotName annotation) { diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/InvalidJsonFromServerTest.java b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/InvalidJsonFromServerTest.java new file mode 100644 index 0000000000000..7e9ce98150490 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/InvalidJsonFromServerTest.java @@ -0,0 +1,61 @@ +package io.quarkus.rest.client.reactive.jackson.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.reactive.ClientWebApplicationException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests that when the server responds with data that is not valid JSON, we return an internal server error + */ +public class InvalidJsonFromServerTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(JsonObject.class, JsonClient.class, InvalidJsonEndpoint.class)); + + @RestClient + JsonClient client; + + @Test + public void test() { + assertThatThrownBy(() -> client.get()) + .isInstanceOf(ClientWebApplicationException.class) + .hasMessageContaining("HTTP 200") + .cause() + .hasMessageContaining("was expecting double-quote to start field name"); + } + + @Path("/invalid-json") + @RegisterRestClient(baseUri = "http://localhost:8081") + public interface JsonClient { + + @Produces(MediaType.APPLICATION_JSON) + @GET + JsonObject get(); + } + + static class JsonObject { + public String name; + } + + @Path("/invalid-json") + @Produces(MediaType.APPLICATION_JSON) + public static class InvalidJsonEndpoint { + + @GET + public String get() { + return "{name: test}"; + } + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java b/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java index 19d3d79d72b64..5939422b47b3a 100644 --- a/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java @@ -14,11 +14,13 @@ import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; +import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.ClientWebApplicationException; import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader; +import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.exc.StreamReadException; import com.fasterxml.jackson.databind.DatabindException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,6 +28,8 @@ public class ClientJacksonMessageBodyReader extends JacksonBasicMessageBodyReader implements ClientRestHandler { + private static final Logger log = Logger.getLogger(ClientJacksonMessageBodyReader.class); + private final ConcurrentMap contextResolverMap = new ConcurrentHashMap<>(); private RestClientRequestContext context; @@ -39,8 +43,11 @@ public Object readFrom(Class type, Type genericType, Annotation[] annota MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { try { return super.readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream); + } catch (JsonParseException e) { + log.debug("Server returned invalid json data", e); + throw new ClientWebApplicationException(e, Response.Status.OK); } catch (StreamReadException | DatabindException e) { - throw new ClientWebApplicationException(e, Response.Status.BAD_REQUEST); + throw new ClientWebApplicationException(e, Response.Status.BAD_REQUEST); // TODO: we need to check if this actually makes sense... } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/ClientExceptionMapperHandler.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/ClientExceptionMapperHandler.java index 583aa2cfa6883..ae97af6d7c3b3 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/ClientExceptionMapperHandler.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/ClientExceptionMapperHandler.java @@ -90,12 +90,6 @@ GeneratedClassResult generateResponseExceptionMapper(AnnotationInstance instance throw new IllegalStateException(message); } - StringBuilder sigBuilder = new StringBuilder(); - sigBuilder.append(targetMethod.name()).append("_").append(targetMethod.returnType().name().toString()); - for (Type i : targetMethod.parameterTypes()) { - sigBuilder.append(i.name().toString()); - } - int priority = Priorities.USER; AnnotationValue priorityAnnotationValue = instance.value("priority"); if (priorityAnnotationValue != null) { @@ -103,8 +97,7 @@ GeneratedClassResult generateResponseExceptionMapper(AnnotationInstance instance } ClassInfo restClientInterfaceClassInfo = targetMethod.declaringClass(); - String generatedClassName = restClientInterfaceClassInfo.name().toString() + "_" + targetMethod.name() + "_" - + "ResponseExceptionMapper" + "_" + HashUtil.sha1(sigBuilder.toString()); + String generatedClassName = getGeneratedClassName(targetMethod); try (ClassCreator cc = ClassCreator.builder().classOutput(classOutput).className(generatedClassName) .interfaces(ResteasyReactiveResponseExceptionMapper.class).build()) { MethodCreator toThrowable = cc.getMethodCreator("toThrowable", Throwable.class, Response.class, @@ -143,6 +136,17 @@ GeneratedClassResult generateResponseExceptionMapper(AnnotationInstance instance return new GeneratedClassResult(restClientInterfaceClassInfo.name().toString(), generatedClassName, priority); } + public static String getGeneratedClassName(MethodInfo methodInfo) { + StringBuilder sigBuilder = new StringBuilder(); + sigBuilder.append(methodInfo.name()).append("_").append(methodInfo.returnType().name().toString()); + for (Type i : methodInfo.parameterTypes()) { + sigBuilder.append(i.name().toString()); + } + + return methodInfo.declaringClass().name().toString() + "_" + methodInfo.name() + "_" + + "ResponseExceptionMapper" + "_" + HashUtil.sha1(sigBuilder.toString()); + } + private static boolean ignoreAnnotation(MethodInfo methodInfo) { // ignore the annotation if it's placed on a Kotlin companion class // this is not a problem since the Kotlin compiler will also place the annotation the static method interface method diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java index 1d0b3f5d45d2c..df1a36f223dec 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/DotNames.java @@ -10,6 +10,7 @@ import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.annotation.RegisterProviders; +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; import org.jboss.jandex.DotName; import io.quarkus.rest.client.reactive.ClientExceptionMapper; @@ -32,6 +33,8 @@ public class DotNames { public static final DotName CLIENT_EXCEPTION_MAPPER = DotName.createSimple(ClientExceptionMapper.class.getName()); public static final DotName CLIENT_REDIRECT_HANDLER = DotName.createSimple(ClientRedirectHandler.class.getName()); + public static final DotName RESPONSE_EXCEPTION_MAPPER = DotName.createSimple(ResponseExceptionMapper.class.getName()); + static final DotName METHOD = DotName.createSimple(Method.class.getName()); private DotNames() { diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java index 10beeb6e130d9..7bace127961f7 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java @@ -10,9 +10,12 @@ import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_CLIENT_HEADERS; import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_PROVIDER; import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_PROVIDERS; +import static io.quarkus.rest.client.reactive.deployment.DotNames.RESPONSE_EXCEPTION_MAPPER; import static java.util.Arrays.asList; import static java.util.stream.Collectors.*; import static org.jboss.resteasy.reactive.common.processor.EndpointIndexer.CDI_WRAPPER_SUFFIX; +import static org.jboss.resteasy.reactive.common.processor.JandexUtil.isImplementorOf; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.BLOCKING; import static org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveScanner.BUILTIN_HTTP_ANNOTATIONS_TO_METHOD; import java.lang.annotation.RetentionPolicy; @@ -498,6 +501,24 @@ void addRestClientBeans(Capabilities capabilities, } } } + + Set blockingClassNames = new HashSet<>(); + Set registerBlockingClasses = new HashSet<>(index.getAnnotations(BLOCKING)); + for (AnnotationInstance registerBlockingClass : registerBlockingClasses) { + AnnotationTarget target = registerBlockingClass.target(); + if (target.kind() == AnnotationTarget.Kind.CLASS + && isImplementorOf(index, target.asClass(), RESPONSE_EXCEPTION_MAPPER)) { + // Watch for @Blocking annotations in classes that implements ResponseExceptionMapper: + blockingClassNames.add(target.asClass().toString()); + } else if (target.kind() == AnnotationTarget.Kind.METHOD + && target.asMethod().annotation(CLIENT_EXCEPTION_MAPPER) != null) { + // Watch for @Blocking annotations in methods that are also annotated with @ClientExceptionMapper: + blockingClassNames.add(ClientExceptionMapperHandler.getGeneratedClassName(target.asMethod())); + } + } + + recorder.setBlockingClassNames(blockingClassNames); + if (LaunchMode.current() == LaunchMode.DEVELOPMENT) { recorder.setConfigKeys(configKeys); } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/converter/ParamConverterProviderTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/converter/ParamConverterProviderTest.java index 535ea3ebe2065..b56ac980cadb0 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/converter/ParamConverterProviderTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/converter/ParamConverterProviderTest.java @@ -2,10 +2,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Type; import java.net.URI; +import java.util.Arrays; import jakarta.ws.rs.BeanParam; import jakarta.ws.rs.CookieParam; @@ -74,7 +80,7 @@ void shouldConvertHeaderParams() { void shouldConvertCookieParams() { Client client = RestClientBuilder.newBuilder().baseUri(baseUri) .build(Client.class); - assertThat(client.getWithHeader(Param.FIRST)).isEqualTo("1"); + assertThat(client.getWithCookie(Param.FIRST)).isEqualTo("1"); assertThat(client.sub().getWithCookie(Param.SECOND)).isEqualTo("2"); Bean bean = new Bean(); @@ -93,7 +99,7 @@ interface Client { @GET @Path("/param/{param}") - String get(@PathParam("param") Param param); + String get(@MyAnnotation("myValue") @PathParam("param") Param param); @GET @Path("/param/{param}") @@ -101,7 +107,7 @@ interface Client { @GET @Path("/query") - String getWithQuery(@QueryParam("param") Param param); + String getWithQuery(@MyAnnotation("myValue") @QueryParam("param") Param param); @GET @Path("/query") @@ -109,7 +115,7 @@ interface Client { @GET @Path("/header") - String getWithHeader(@HeaderParam("param") Param param); + String getWithHeader(@MyAnnotation("myValue") @HeaderParam("param") Param param); @GET @Path("/header") @@ -117,7 +123,7 @@ interface Client { @GET @Path("/cookie") - String getWithCookie(@HeaderParam("cookie-param") Param param); + String getWithCookie(@MyAnnotation("myValue") @CookieParam("cookie-param") Param param); @GET @Path("/cookie") @@ -127,28 +133,32 @@ interface Client { interface SubClient { @GET @Path("/param/{param}") - String get(@PathParam("param") Param param); + String get(@MyAnnotation("myValue") @PathParam("param") Param param); @GET @Path("/query") - String getWithQuery(@QueryParam("param") Param param); + String getWithQuery(@MyAnnotation("myValue") @QueryParam("param") Param param); @GET @Path("/header") - String getWithHeader(@HeaderParam("param") Param param); + String getWithHeader(@MyAnnotation("myValue") @HeaderParam("param") Param param); @GET @Path("cookie") - String getWithCookie(@CookieParam("cookie-param") Param param); + String getWithCookie(@MyAnnotation("myValue") @CookieParam("cookie-param") Param param); } public static class Bean { + @MyAnnotation("myValue") @PathParam("param") public Param param; + @MyAnnotation("myValue") @QueryParam("param") public Param queryParam; + @MyAnnotation("myValue") @HeaderParam("param") public Param headerParam; + @MyAnnotation("myValue") @CookieParam("cookie-param") public Param cookieParam; } @@ -158,6 +168,12 @@ enum Param { SECOND } + @Target({ ElementType.FIELD, ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + public @interface MyAnnotation { + String value() default ""; + } + public static class ParamConverter implements ParamConverterProvider { @SuppressWarnings("unchecked") @Override @@ -171,6 +187,9 @@ public jakarta.ws.rs.ext.ParamConverter getConverter(Class rawType, Ty fail("Annotations cannot be null!"); } + assertTrue(Arrays.stream(annotations) + .anyMatch(a -> a instanceof MyAnnotation && ((MyAnnotation) a).value().equals("myValue"))); + if (rawType == Param.class) { return (jakarta.ws.rs.ext.ParamConverter) new jakarta.ws.rs.ext.ParamConverter() { @Override diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/error/BlockingExceptionMapperTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/error/BlockingExceptionMapperTest.java new file mode 100644 index 0000000000000..47b82d5993330 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/error/BlockingExceptionMapperTest.java @@ -0,0 +1,253 @@ +package io.quarkus.rest.client.reactive.error; + +import static io.quarkus.rest.client.reactive.RestClientTestUtil.setUrlForClass; +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.reactive.common.core.BlockingNotAllowedException; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.rest.client.reactive.ClientExceptionMapper; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.common.annotation.Blocking; +import io.vertx.core.Context; + +public class BlockingExceptionMapperTest { + + private static final AtomicBoolean EVENT_LOOP_THREAD_USED_BY_MAPPER = new AtomicBoolean(); + private static final int STATUS_FOR_BLOCKING_MAPPER = 501; + private static final int STATUS_FOR_NON_BLOCKING_MAPPER = 500; + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Client.class, + ClientUsingNotBlockingExceptionMapper.class, + ClientUsingBlockingExceptionMapper.class, + ClientUsingBlockingExceptionMapperWithAnnotation.class, + ClientUsingBothExceptionMappers.class, + NotBlockingExceptionMapper.class, + BlockingExceptionMapper.class, + ClientResource.class, + Resource.class) + .addAsResource( + new StringAsset(setUrlForClass(ClientUsingNotBlockingExceptionMapper.class) + "\n" + + setUrlForClass(ClientUsingBlockingExceptionMapper.class) + "\n" + + setUrlForClass(ClientUsingBlockingExceptionMapperWithAnnotation.class) + "\n" + + setUrlForClass(ClientUsingBothExceptionMappers.class) + "\n"), + "application.properties")); + + public static final String ERROR_MESSAGE = "The entity was not found"; + + @RestClient + ClientUsingNotBlockingExceptionMapper clientUsingNotBlockingExceptionMapper; + + @RestClient + ClientUsingBlockingExceptionMapper clientUsingBlockingExceptionMapper; + + @RestClient + ClientUsingBlockingExceptionMapperWithAnnotation clientUsingBlockingExceptionMapperWithAnnotation; + + @RestClient + ClientUsingBothExceptionMappers clientUsingBothExceptionMappers; + + @BeforeEach + public void setup() { + EVENT_LOOP_THREAD_USED_BY_MAPPER.set(false); + } + + @Disabled("This test randomly fails because https://github.com/quarkusio/quarkus/issues/32839") + @Test + public void shouldUseEventLoopByDefault() { + assertThrows(BlockingNotAllowedException.class, clientUsingNotBlockingExceptionMapper::nonBlocking); + assertThat(EVENT_LOOP_THREAD_USED_BY_MAPPER.get()).isTrue(); + } + + @Test + public void shouldUseWorkerThreadIfExceptionMapperIsAnnotatedWithBlocking() { + RuntimeException exception = assertThrows(RuntimeException.class, clientUsingBlockingExceptionMapper::blocking); + assertThat(EVENT_LOOP_THREAD_USED_BY_MAPPER.get()).isFalse(); + assertThat(exception.getMessage()).isEqualTo(ERROR_MESSAGE); + } + + @Test + public void shouldUseWorkerThreadOnlyIfExceptionMapperIsAnnotatedWithBlockingIsUsed() { + // To be uncommented after https://github.com/quarkusio/quarkus/issues/32839 is fixed: + // assertThrows(BlockingNotAllowedException.class, clientUsingBothExceptionMappers::nonBlocking); + // assertThat(EVENT_LOOP_THREAD_USED_BY_MAPPER.get()).isTrue(); + + RuntimeException exception = assertThrows(RuntimeException.class, clientUsingBothExceptionMappers::blocking); + assertThat(EVENT_LOOP_THREAD_USED_BY_MAPPER.get()).isFalse(); + assertThat(exception.getMessage()).isEqualTo(ERROR_MESSAGE); + } + + @Test + public void shouldUseWorkerThreadWhenClientIsInjected() { + // To be uncommented after https://github.com/quarkusio/quarkus/issues/32839 is fixed: + // given().get("/client/non-blocking").then().statusCode(500); + // assertThat(EVENT_LOOP_THREAD_USED_BY_MAPPER.get()).isTrue(); + + given().get("/client/blocking").then().statusCode(500); + assertThat(EVENT_LOOP_THREAD_USED_BY_MAPPER.get()).isFalse(); + } + + @Test + public void shouldUseWorkerThreadIfExceptionMapperIsAnnotatedWithBlockingAndUsingClientExceptionMapper() { + RuntimeException exception = assertThrows(RuntimeException.class, + clientUsingBlockingExceptionMapperWithAnnotation::blocking); + assertThat(EVENT_LOOP_THREAD_USED_BY_MAPPER.get()).isFalse(); + assertThat(exception.getMessage()).isEqualTo(ERROR_MESSAGE); + } + + @Test + public void shouldUseWorkerThreadUsingProgrammaticApproach() { + var client = RestClientBuilder.newBuilder() + .baseUri(UriBuilder.fromUri("http://localhost:8081").build()) + .register(BlockingExceptionMapper.class) + .build(Client.class); + + RuntimeException exception = assertThrows(RuntimeException.class, client::blocking); + assertThat(EVENT_LOOP_THREAD_USED_BY_MAPPER.get()).isFalse(); + assertThat(exception.getMessage()).isEqualTo(ERROR_MESSAGE); + } + + @Path("/error") + @RegisterRestClient + public interface Client { + @GET + @Path("/blocking") + InputStream blocking(); + } + + @Path("/error") + @RegisterRestClient + @RegisterProvider(NotBlockingExceptionMapper.class) + public interface ClientUsingNotBlockingExceptionMapper { + + @GET + @Path("/non-blocking") + InputStream nonBlocking(); + } + + @Path("/error") + @RegisterRestClient + @RegisterProvider(BlockingExceptionMapper.class) + public interface ClientUsingBlockingExceptionMapper { + @GET + @Path("/blocking") + InputStream blocking(); + } + + @Path("/error") + @RegisterRestClient + @RegisterProvider(NotBlockingExceptionMapper.class) + @RegisterProvider(BlockingExceptionMapper.class) + public interface ClientUsingBothExceptionMappers { + @GET + @Path("/blocking") + InputStream blocking(); + + @GET + @Path("/non-blocking") + InputStream nonBlocking(); + } + + @Path("/error") + @RegisterRestClient + public interface ClientUsingBlockingExceptionMapperWithAnnotation { + @GET + @Path("/blocking") + InputStream blocking(); + + @Blocking + @ClientExceptionMapper + static RuntimeException map(Response response) { + EVENT_LOOP_THREAD_USED_BY_MAPPER.set(Context.isOnEventLoopThread()); + return new RuntimeException(response.readEntity(String.class)); + } + } + + public static class NotBlockingExceptionMapper implements ResponseExceptionMapper { + + @Override + public boolean handles(int status, MultivaluedMap headers) { + return status == STATUS_FOR_NON_BLOCKING_MAPPER; + } + + @Override + public Exception toThrowable(Response response) { + EVENT_LOOP_THREAD_USED_BY_MAPPER.set(Context.isOnEventLoopThread()); + // Reading InputStream in the Event Loop throws the BlockingNotAllowedException exception + response.readEntity(String.class); + return null; + } + } + + @Blocking + public static class BlockingExceptionMapper implements ResponseExceptionMapper { + @Override + public boolean handles(int status, MultivaluedMap headers) { + return status == STATUS_FOR_BLOCKING_MAPPER; + } + + @Override + public Exception toThrowable(Response response) { + EVENT_LOOP_THREAD_USED_BY_MAPPER.set(Context.isOnEventLoopThread()); + return new RuntimeException(response.readEntity(String.class)); + } + } + + @Path("/error") + public static class Resource { + + @GET + @Path("/blocking") + public Response blocking() { + return Response.status(STATUS_FOR_BLOCKING_MAPPER).entity(ERROR_MESSAGE).build(); + } + + @GET + @Path("/non-blocking") + public Response nonBlocking() { + return Response.status(STATUS_FOR_NON_BLOCKING_MAPPER).entity(ERROR_MESSAGE).build(); + } + } + + @Path("/client") + public static class ClientResource { + + @RestClient + ClientUsingBothExceptionMappers clientUsingBothExceptionMappers; + + @GET + @Path("/blocking") + public void callBlocking() { + clientUsingBothExceptionMappers.blocking(); + } + + @GET + @Path("/non-blocking") + public void callNonBlocking() { + clientUsingBothExceptionMappers.nonBlocking(); + } + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/ClientUseWorkerExecutorRestHandler.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/ClientUseWorkerExecutorRestHandler.java new file mode 100644 index 0000000000000..d8cf3b8305253 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/ClientUseWorkerExecutorRestHandler.java @@ -0,0 +1,41 @@ +package io.quarkus.rest.client.reactive.runtime; + +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; +import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; + +import io.quarkus.runtime.ExecutorRecorder; +import io.vertx.core.Context; + +/** + * This is added by the Reactive Rest Client if the `@Blocking` annotation is used in some scenarios. For example, when users + * provide a custom ResponseExceptionMapper that is annotates with the `@Blocking` annotation. + * + * Then this handler is applied, the execution of the next handlers will use the worker thread pool. + */ +public class ClientUseWorkerExecutorRestHandler implements ClientRestHandler { + + private volatile Executor executor; + private final Supplier supplier = new Supplier() { + @Override + public Executor get() { + return ExecutorRecorder.getCurrent(); + } + }; + + @Override + public void handle(RestClientRequestContext requestContext) throws Exception { + if (!Context.isOnEventLoopThread()) { + return; //already dispatched + } + + if (executor == null) { + executor = supplier.get(); + } + + requestContext.suspend(); + requestContext.resume(executor); + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientResponseFilter.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientResponseFilter.java index 8e78ea877f6df..1d369c71e5a47 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientResponseFilter.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientResponseFilter.java @@ -1,6 +1,7 @@ package io.quarkus.rest.client.reactive.runtime; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import jakarta.ws.rs.client.ClientRequestContext; @@ -11,10 +12,14 @@ import org.jboss.resteasy.reactive.client.handlers.ClientResponseCompleteRestHandler; import org.jboss.resteasy.reactive.client.impl.ClientRequestContextImpl; import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; +import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; import org.jboss.resteasy.reactive.common.core.UnwrappableException; import org.jboss.resteasy.reactive.common.jaxrs.ResponseImpl; +import io.vertx.core.Context; + public class MicroProfileRestClientResponseFilter implements ClientResponseFilter { + private static final ClientRestHandler[] EMPTY_CLIENT_REST_HANDLERS = new ClientRestHandler[0]; private final List> exceptionMappers; public MicroProfileRestClientResponseFilter(List> exceptionMappers) { @@ -29,21 +34,47 @@ public MicroProfileRestClientResponseFilter(List> exc public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { for (ResponseExceptionMapper exceptionMapper : exceptionMappers) { if (exceptionMapper.handles(responseContext.getStatus(), responseContext.getHeaders())) { - // we have an exception mapper, we don't need the response anymore, we can map it to response right away (I hope :D) RestClientRequestContext restClientContext = ((ClientRequestContextImpl) requestContext) .getRestClientRequestContext(); - ResponseImpl response = ClientResponseCompleteRestHandler.mapToResponse(restClientContext, false); - Throwable throwable; - if (exceptionMapper instanceof ResteasyReactiveResponseExceptionMapper) { - throwable = ((ResteasyReactiveResponseExceptionMapper) exceptionMapper).toThrowable(response, - restClientContext); + + boolean requiresBlocking = RestClientRecorder.isClassBlocking(exceptionMapper.getClass()); + if (Context.isOnEventLoopThread() && requiresBlocking) { + switchToWorkerThreadPoolAndRetry(restClientContext); + break; } else { - throwable = exceptionMapper.toThrowable(response); - } - if (throwable != null) { - throw new UnwrappableException(throwable); + // we have an exception mapper, we don't need the response anymore, we can map it to response right away (I hope :D) + ResponseImpl response = ClientResponseCompleteRestHandler.mapToResponse(restClientContext, false); + Throwable throwable; + if (exceptionMapper instanceof ResteasyReactiveResponseExceptionMapper) { + throwable = ((ResteasyReactiveResponseExceptionMapper) exceptionMapper).toThrowable(response, + restClientContext); + } else { + throwable = exceptionMapper.toThrowable(response); + } + if (throwable != null) { + throw new UnwrappableException(throwable); + } } } } } + + private void switchToWorkerThreadPoolAndRetry(RestClientRequestContext restClientContext) { + int position = restClientContext.getPosition(); + + List nextHandlers = new ArrayList<>(2 + restClientContext.getHandlers().length - position); + nextHandlers.add(new ClientUseWorkerExecutorRestHandler()); + nextHandlers.add(currentHandler(restClientContext)); + + while (position < restClientContext.getHandlers().length) { + nextHandlers.add(restClientContext.getHandlers()[position]); + position++; + } + + restClientContext.restart(nextHandlers.toArray(EMPTY_CLIENT_REST_HANDLERS), true); + } + + private ClientRestHandler currentHandler(RestClientRequestContext restClientContext) { + return restClientContext.getHandlers()[restClientContext.getPosition() - 1]; + } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientRecorder.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientRecorder.java index e310af1d53137..6049e880488f1 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientRecorder.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientRecorder.java @@ -1,6 +1,7 @@ package io.quarkus.rest.client.reactive.runtime; import java.util.Map; +import java.util.Set; import org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver; @@ -9,15 +10,24 @@ @Recorder public class RestClientRecorder { private static volatile Map configKeys; + private static volatile Set blockingClassNames; public void setConfigKeys(Map configKeys) { RestClientRecorder.configKeys = configKeys; } + public void setBlockingClassNames(Set blockingClassNames) { + RestClientRecorder.blockingClassNames = blockingClassNames; + } + public static Map getConfigKeys() { return configKeys; } + public static boolean isClassBlocking(Class exceptionMapperClass) { + return blockingClassNames.contains(exceptionMapperClass.getName()); + } + public void setRestClientBuilderResolver() { RestClientBuilderResolver.setInstance(new BuilderResolver()); } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index a240bf2cabc32..1b9a6f21932e7 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -214,6 +214,11 @@ private static void prepareBouncyCastleProvider(CurateOutcomeBuildItem curateOut .produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.jcajce.provider.drbg.DRBG$Default")); runtimeReInitialized .produce(new RuntimeReinitializedClassBuildItem("org.bouncycastle.jcajce.provider.drbg.DRBG$NonceAndIV")); + // URLSeededEntropySourceProvider.seedStream may contain a reference to a 'FileInputStream' which includes + // references to FileDescriptors which aren't allowed in the image heap + runtimeReInitialized + .produce(new RuntimeReinitializedClassBuildItem( + "org.bouncycastle.jcajce.provider.drbg.DRBG$URLSeededEntropySourceProvider")); } else { reflection.produce(ReflectiveClassBuildItem.builder("org.bouncycastle.crypto.general.AES") .methods().fields().build()); diff --git a/extensions/smallrye-stork/deployment/src/main/java/io/quarkus/stork/deployment/SmallRyeStorkProcessor.java b/extensions/smallrye-stork/deployment/src/main/java/io/quarkus/stork/deployment/SmallRyeStorkProcessor.java index 39254abe27a81..a1e2f4ab5750a 100644 --- a/extensions/smallrye-stork/deployment/src/main/java/io/quarkus/stork/deployment/SmallRyeStorkProcessor.java +++ b/extensions/smallrye-stork/deployment/src/main/java/io/quarkus/stork/deployment/SmallRyeStorkProcessor.java @@ -26,8 +26,10 @@ import io.quarkus.vertx.deployment.VertxBuildItem; import io.smallrye.stork.spi.LoadBalancerProvider; import io.smallrye.stork.spi.ServiceDiscoveryProvider; +import io.smallrye.stork.spi.ServiceRegistrarProvider; import io.smallrye.stork.spi.internal.LoadBalancerLoader; import io.smallrye.stork.spi.internal.ServiceDiscoveryLoader; +import io.smallrye.stork.spi.internal.ServiceRegistrarLoader; public class SmallRyeStorkProcessor { @@ -35,10 +37,11 @@ public class SmallRyeStorkProcessor { private static final Logger LOGGER = Logger.getLogger(SmallRyeStorkProcessor.class.getName()); @BuildStep - void registerServiceProviders(BuildProducer services, Capabilities capabilities) { + void registerServiceProviders(BuildProducer services) { services.produce(new ServiceProviderBuildItem(io.smallrye.stork.spi.config.ConfigProvider.class.getName(), StorkConfigProvider.class.getName())); - for (Class providerClass : asList(LoadBalancerLoader.class, ServiceDiscoveryLoader.class)) { + for (Class providerClass : asList(LoadBalancerLoader.class, ServiceDiscoveryLoader.class, + ServiceRegistrarLoader.class)) { services.produce(ServiceProviderBuildItem.allProvidersFromClassPath(providerClass.getName())); } } @@ -47,11 +50,15 @@ void registerServiceProviders(BuildProducer services, UnremovableBeanBuildItem unremoveableBeans() { return UnremovableBeanBuildItem.beanTypes( DotName.createSimple(ServiceDiscoveryProvider.class), - DotName.createSimple(LoadBalancerProvider.class)); + DotName.createSimple(ServiceDiscoveryLoader.class), + DotName.createSimple(LoadBalancerProvider.class), + DotName.createSimple(LoadBalancerLoader.class), + DotName.createSimple(ServiceRegistrarProvider.class), + DotName.createSimple(ServiceRegistrarLoader.class)); } /** - * This build step is the fix for https://github.com/quarkusio/quarkus/issues/24444. + * This build step is the fix for #24444. * Because Stork itself cannot depend on Quarkus, and we do not want to have extensions for all the service * discovery and load-balancer providers, we work around the issue by detecting when the kubernetes service * discovery is used and if the kubernetes extension is used. diff --git a/extensions/undertow/spi/src/main/java/io/quarkus/undertow/deployment/UndertowStaticResourcesBuildStep.java b/extensions/undertow/spi/src/main/java/io/quarkus/undertow/deployment/UndertowStaticResourcesBuildStep.java index f0db4f8b07070..5846c627da36b 100644 --- a/extensions/undertow/spi/src/main/java/io/quarkus/undertow/deployment/UndertowStaticResourcesBuildStep.java +++ b/extensions/undertow/spi/src/main/java/io/quarkus/undertow/deployment/UndertowStaticResourcesBuildStep.java @@ -12,6 +12,8 @@ import java.util.List; import java.util.Set; +import io.quarkus.bootstrap.classloading.ClassPathElement; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -22,7 +24,6 @@ import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.runtime.LaunchMode; -import io.quarkus.runtime.util.ClassPathUtils; /** * NOTE: Shared with Resteasy standalone! @@ -56,8 +57,8 @@ void scanStaticResources(Capabilities capabilities, ApplicationArchivesBuildItem } //we need to check for web resources in order to get welcome files to work //this kinda sucks - Set knownFiles = new HashSet<>(); - Set knownDirectories = new HashSet<>(); + final Set knownFiles = new HashSet<>(); + final Set knownDirectories = new HashSet<>(); for (ApplicationArchive i : applicationArchivesBuildItem.getAllApplicationArchives()) { i.accept(tree -> { Path resource = tree.getPath(META_INF_RESOURCES); @@ -67,9 +68,14 @@ void scanStaticResources(Capabilities capabilities, ApplicationArchivesBuildItem }); } - ClassPathUtils.consumeAsPaths(META_INF_RESOURCES, resource -> { - collectKnownPaths(resource, knownFiles, knownDirectories); - }); + for (ClassPathElement e : QuarkusClassLoader.getElements(META_INF_RESOURCES, false)) { + if (e.isRuntime()) { + e.apply(tree -> { + collectKnownPaths(tree.getPath(META_INF_RESOURCES), knownFiles, knownDirectories); + return null; + }); + } + } for (GeneratedWebResourceBuildItem genResource : generatedWebResources) { String sub = genResource.getName(); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java index 78074c4e63ed4..e95d614d3389a 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java @@ -1,5 +1,20 @@ package io.quarkus.devui.deployment; +import static java.util.logging.Level.ALL; +import static java.util.logging.Level.CONFIG; +import static java.util.logging.Level.FINE; +import static java.util.logging.Level.FINER; +import static java.util.logging.Level.FINEST; +import static java.util.logging.Level.OFF; +import static java.util.logging.Level.SEVERE; +import static java.util.logging.Level.WARNING; +import static org.jboss.logmanager.Level.DEBUG; +import static org.jboss.logmanager.Level.ERROR; +import static org.jboss.logmanager.Level.FATAL; +import static org.jboss.logmanager.Level.INFO; +import static org.jboss.logmanager.Level.TRACE; +import static org.jboss.logmanager.Level.WARN; + import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -32,7 +47,10 @@ import io.quarkus.devui.spi.DevUIContent; import io.quarkus.devui.spi.buildtime.QuteTemplateBuildItem; import io.quarkus.devui.spi.buildtime.StaticContentBuildItem; +import io.quarkus.devui.spi.page.AbstractPageBuildItem; import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.FooterPageBuildItem; +import io.quarkus.devui.spi.page.MenuPageBuildItem; import io.quarkus.devui.spi.page.Page; import io.quarkus.devui.spi.page.PageBuilder; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; @@ -67,6 +85,8 @@ InternalImportMapBuildItem createKnownInternalImportMap(NonApplicationRootPathBu internalImportMapBuildItem.add("qwc-server-log", contextRoot + "qwc/qwc-server-log.js"); // Quarkus UI internalImportMapBuildItem.add("qui/", contextRoot + "qui/"); + internalImportMapBuildItem.add("qui-card", contextRoot + "qui/qui-card.js"); + internalImportMapBuildItem.add("qui-badge", contextRoot + "qui/qui-badge.js"); internalImportMapBuildItem.add("qui-alert", contextRoot + "qui/qui-alert.js"); internalImportMapBuildItem.add("qui-code-block", contextRoot + "qui/qui-code-block.js"); @@ -103,13 +123,31 @@ InternalImportMapBuildItem createKnownInternalImportMap(NonApplicationRootPathBu * @param buildTimeConstProducer */ @BuildStep(onlyIf = IsDevelopment.class) - void mapPageBuildTimeData(List pageBuildItems, + void mapPageBuildTimeData(List cards, + List menus, + List footers, CurateOutcomeBuildItem curateOutcomeBuildItem, BuildProducer buildTimeConstProducer) { - for (CardPageBuildItem pageBuildItem : pageBuildItems) { - String extensionPathName = pageBuildItem.getExtensionPathName(curateOutcomeBuildItem); - Map buildTimeData = getBuildTimeData(curateOutcomeBuildItem, pageBuildItem); + for (CardPageBuildItem card : cards) { + String extensionPathName = card.getExtensionPathName(curateOutcomeBuildItem); + Map buildTimeData = getBuildTimeDataForCard(curateOutcomeBuildItem, card); + if (!buildTimeData.isEmpty()) { + buildTimeConstProducer.produce( + new BuildTimeConstBuildItem(extensionPathName, buildTimeData)); + } + } + for (MenuPageBuildItem menu : menus) { + String extensionPathName = menu.getExtensionPathName(curateOutcomeBuildItem); + Map buildTimeData = getBuildTimeDataForPage(menu); + if (!buildTimeData.isEmpty()) { + buildTimeConstProducer.produce( + new BuildTimeConstBuildItem(extensionPathName, buildTimeData)); + } + } + for (FooterPageBuildItem footer : footers) { + String extensionPathName = footer.getExtensionPathName(curateOutcomeBuildItem); + Map buildTimeData = getBuildTimeDataForPage(footer); if (!buildTimeData.isEmpty()) { buildTimeConstProducer.produce( new BuildTimeConstBuildItem(extensionPathName, buildTimeData)); @@ -117,12 +155,17 @@ void mapPageBuildTimeData(List pageBuildItems, } } - private Map getBuildTimeData(CurateOutcomeBuildItem curateOutcomeBuildItem, - CardPageBuildItem pageBuildItem) { + private Map getBuildTimeDataForPage(AbstractPageBuildItem pageBuildItem) { Map m = new HashMap<>(); if (pageBuildItem.hasBuildTimeData()) { m.putAll(pageBuildItem.getBuildTimeData()); } + return m; + } + + private Map getBuildTimeDataForCard(CurateOutcomeBuildItem curateOutcomeBuildItem, + CardPageBuildItem pageBuildItem) { + Map m = getBuildTimeDataForPage(pageBuildItem); if (pageBuildItem.getOptionalCard().isPresent()) { // Make the pages available for the custom card @@ -383,6 +426,7 @@ private void addFooterTabBuildTimeData(BuildTimeConstBuildItem internalBuildTime } internalBuildTimeData.addBuildTimeData("footerTabs", footerTabs); + internalBuildTimeData.addBuildTimeData("loggerLevels", LEVELS); } private void addVersionInfoBuildTimeData(BuildTimeConstBuildItem internalBuildTimeData, @@ -398,6 +442,22 @@ private void addVersionInfoBuildTimeData(BuildTimeConstBuildItem internalBuildTi internalBuildTimeData.addBuildTimeData("applicationInfo", applicationInfo); } + private static final List LEVELS = List.of( + OFF.getName(), + SEVERE.getName(), + ERROR.getName(), + FATAL.getName(), + WARNING.getName(), + WARN.getName(), + INFO.getName(), + DEBUG.getName(), + TRACE.getName(), + CONFIG.getName(), + FINE.getName(), + FINER.getName(), + FINEST.getName(), + ALL.getName()); + private static void computeColors(Map> themes, Map dark, Map light) { // Quarkus logo colors diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java index 4c2e6707cb3d3..f57d61b6f898b 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java @@ -38,6 +38,7 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.dev.console.DevConsoleManager; @@ -124,9 +125,14 @@ void registerDevUiHandlers( List staticContentBuildItems, BuildProducer routeProducer, DevUIRecorder recorder, + LaunchModeBuildItem launchModeBuildItem, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, ShutdownContextBuildItem shutdownContext) throws IOException { + if (launchModeBuildItem.isNotLocalDevModeType()) { + return; + } + // Websocket for JsonRPC comms routeProducer.produce( nonApplicationRootPathBuildItem @@ -215,6 +221,7 @@ void registerDevUiHandlers( void additionalBean(BuildProducer additionalBeanProducer, BuildProducer additionalIndexProducer, List jsonRPCProvidersBuildItems) { + additionalBeanProducer.produce(AdditionalBeanBuildItem.builder() .addBeanClass(JsonRpcRouter.class) .setUnremovable().build()); @@ -244,10 +251,15 @@ void additionalBean(BuildProducer additionalBeanProduce @BuildStep(onlyIf = IsDevelopment.class) void findAllJsonRPCMethods(BuildProducer jsonRPCMethodsProvider, BuildProducer buildTimeConstProducer, + LaunchModeBuildItem launchModeBuildItem, CombinedIndexBuildItem combinedIndexBuildItem, CurateOutcomeBuildItem curateOutcomeBuildItem, List jsonRPCProvidersBuildItems) { + if (launchModeBuildItem.isNotLocalDevModeType()) { + return; + } + IndexView index = combinedIndexBuildItem.getIndex(); Map> extensionMethodsMap = new HashMap<>(); // All methods so that we can build the reflection @@ -354,11 +366,19 @@ void createJsonRpcRouter(DevUIRecorder recorder, void getAllExtensions(List cardPageBuildItems, List menuPageBuildItems, List footerPageBuildItems, + LaunchModeBuildItem launchModeBuildItem, CurateOutcomeBuildItem curateOutcomeBuildItem, BuildProducer extensionsProducer, BuildProducer webJarBuildProducer, BuildProducer devUIWebJarProducer) { + if (launchModeBuildItem.isNotLocalDevModeType()) { + // produce extension build item as cascade of build steps rely on it + var emptyExtensionBuildItem = new ExtensionsBuildItem(List.of(), List.of(), List.of(), List.of()); + extensionsProducer.produce(emptyExtensionBuildItem); + return; + } + // First create the static resources for our own internal components webJarBuildProducer.produce(WebJarBuildItem.builder() .artifactKey(UI_JAR) @@ -421,7 +441,7 @@ void getAllExtensions(List cardPageBuildItems, if (metaData.containsKey(CAPABILITIES)) { Map capabilities = (Map) metaData.get(CAPABILITIES); - extension.setConfigFilter((List) capabilities.getOrDefault(PROVIDES, null)); + extension.setProvidesCapabilities((List) capabilities.getOrDefault(PROVIDES, null)); } if (metaData.containsKey(CODESTART)) { @@ -560,9 +580,14 @@ public WebJarResourcesFilter.FilterResult apply(String fileName, InputStream fil @BuildStep(onlyIf = IsDevelopment.class) void createAllRoutes(WebJarResultsBuildItem webJarResultsBuildItem, + LaunchModeBuildItem launchModeBuildItem, List devUIWebJarBuiltItems, BuildProducer devUIRoutesProducer) { + if (launchModeBuildItem.isNotLocalDevModeType()) { + return; + } + for (DevUIWebJarBuildItem devUIWebJarBuiltItem : devUIWebJarBuiltItems) { WebJarResultsBuildItem.WebJarResult result = webJarResultsBuildItem .byArtifactKey(devUIWebJarBuiltItem.getArtifactKey()); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Extension.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Extension.java index af1056cd1896b..a51a528f569c8 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Extension.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Extension.java @@ -8,9 +8,6 @@ import io.quarkus.devui.spi.page.Page; public class Extension { - private static final String SPACE = " "; - private static final String DASH = "-"; - private String namespace; private String artifact; private String name; @@ -59,10 +56,6 @@ public void setName(String name) { this.name = name; } - // public String getPathName() { - // return name.toLowerCase().replaceAll(SPACE, DASH); - // } - public String getShortName() { return shortName; } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java index ef7764b945efd..ef12ca50856a9 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java @@ -117,8 +117,8 @@ import io.vertx.core.impl.VertxBuilder; import io.vertx.core.impl.VertxInternal; import io.vertx.core.impl.VertxThread; +import io.vertx.core.impl.transports.JDKTransport; import io.vertx.core.net.impl.VertxHandler; -import io.vertx.core.net.impl.transport.Transport; import io.vertx.core.spi.VertxThreadFactory; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; @@ -238,7 +238,7 @@ public VertxThread newVertxThread(Runnable target, String name, boolean worker, return new VertxThread(target, "[DevConsole]" + name, worker, maxExecTime, maxExecTimeUnit); } }); - builder.transport(Transport.JDK); + builder.findTransport(JDKTransport.INSTANCE); builder.init(); return builder.vertx(); } diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js index f3f07f8ca0904..0b17656c7e4fa 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js @@ -84,7 +84,8 @@ export class RouterController { subMenus.push({ "path" : pageRef, - "name" : pageForNamespace.title + "name" : pageForNamespace.title, // deprecate ? + "page" : pageForNamespace }); }); return { @@ -153,10 +154,11 @@ export class RouterController { RouterController.router.addRoutes(routes); } - // TODO: Pass the other parameters along ? + var currentSelection = window.location.pathname; + var currentQueryString = window.location.search; - var relocationRequest = this.getFrom(); + var relocationRequest = this.getQueryParameter("from"); if (relocationRequest) { // We know and already loaded the requested location if (relocationRequest === path) { @@ -187,10 +189,10 @@ export class RouterController { return params; } - getFrom(){ + getQueryParameter(param){ var params = this.getQueryParameters(); if(params){ - return params.from; + return params[param] || null; }else { return null; } diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-card.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-card.js new file mode 100644 index 0000000000000..2ea714978cdc1 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-card.js @@ -0,0 +1,92 @@ +import { LitElement, html, css} from 'lit'; + +/** + * Card UI Component + */ +export class QuiCard extends LitElement { + + static styles = css` + .card { + display: flex; + flex-direction: column; + justify-content: space-between; + border: 1px solid var(--lumo-contrast-10pct); + border-radius: 4px; + filter: brightness(90%); + + } + + .card-header { + font-size: var(--lumo-font-size-l); + line-height: 1; + height: 25px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 10px 10px; + background-color: var(--lumo-contrast-5pct); + border-bottom: 1px solid var(--lumo-contrast-10pct); + } + + .card-footer { + height: 20px; + padding: 10px 10px; + color: var(--lumo-contrast-50pct); + display: flex; + flex-direction: row; + justify-content: space-between; + visibility:hidden; + }`; + + static properties = { + title: {type: String}, + width: {state: true}, + _hasFooter: {state: true}, + }; + + constructor(){ + super(); + this.width = "100%"; + this._hasFooter = false; + } + + connectedCallback() { + super.connectedCallback() + } + + render() { + return html`
    + ${this._headerTemplate()} + + ${this._footerTemplate()} +
    `; + } + + firstUpdated(){ + const footerSlot = this.shadowRoot.querySelector("#footer"); + if (footerSlot && footerSlot.assignedNodes().length>0){ + console.log('No content is available') + this._hasFooter = true; + } + } + + _headerTemplate() { + return html`
    +
    ${this.title}
    +
    + `; + } + + _footerTemplate() { + if(this._hasFooter){ + return html` + + `; + } + } + +} +customElements.define('qui-card', QuiCard); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js index 1e41312e10339..114c84c75e924 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js @@ -1,5 +1,6 @@ import { LitElement, html, css } from 'lit'; import { JsonRpc } from 'jsonrpc'; +import { RouterController } from 'router-controller'; import { until } from 'lit/directives/until.js'; import '@vaadin/grid'; import 'qui/qui-alert.js'; @@ -18,6 +19,7 @@ import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { gridRowDetailsRenderer } from '@vaadin/grid/lit.js'; import { observeState } from 'lit-element-state'; import { devuiState } from 'devui-state'; +import 'qui-badge'; /** * This component allows users to change the configuration @@ -25,6 +27,7 @@ import { devuiState } from 'devui-state'; export class QwcConfiguration extends observeState(LitElement) { jsonRpc = new JsonRpc(this); + routerController = new RouterController(this); static styles = css` .conf { @@ -87,6 +90,13 @@ export class QwcConfiguration extends observeState(LitElement) { constructor() { super(); + + this._filteredValue = this.routerController.getQueryParameter("filter"); + + if(this._filteredValue){ + this._filteredValue = this._filteredValue.replaceAll(",", " OR "); + } + this._filtered = devuiState.allConfiguration; this.jsonRpc.getAllValues().then(e => { this._values = e.result; @@ -103,10 +113,19 @@ export class QwcConfiguration extends observeState(LitElement) { if (! value) { return false; } + if(term.includes(" OR ")){ + let terms = term.split(" OR "); + for (let t of terms) { + if(value.toLowerCase().includes(t.toLowerCase())){ + return true; + } + } + return false; + } return value.toLowerCase().includes(term.toLowerCase()); } - _filter(e) { + _filterTextChanged(e) { const searchTerm = (e.detail.value || '').trim(); if (searchTerm === '') { this._filtered = devuiState.allConfiguration @@ -119,13 +138,15 @@ export class QwcConfiguration extends observeState(LitElement) { } _render() { - if (this._filtered && this._values) { + if (this._filtered && this._values) { return html`
    + @value-changed="${(e) => this._filterTextChanged(e)}"> + ${this._filtered.length} ${this._renderGrid()}
    `; diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js index f54e9d81a5ab0..b5475445e4931 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js @@ -1,39 +1,22 @@ -import {LitElement, html, css} from 'lit'; -import {devServices} from 'devui-data'; -import '@vaadin/vertical-layout'; +import { QwcHotReloadElement, html, css } from 'qwc-hot-reload-element'; +import { devServices } from 'devui-data'; import '@vaadin/icon'; import 'qui-code-block'; +import 'qui-card'; /** * This component shows the Dev Services Page */ -export class QwcDevServices extends LitElement { +export class QwcDevServices extends QwcHotReloadElement { static styles = css` .cards { height: 100%; - } - .card { + padding-right: 10px; display: flex; flex-direction: column; - border: 1px solid var(--lumo-contrast-10pct); - border-radius: 4px; - margin-left: 30px; - margin-right: 30px; - } - - .card-header { - font-size: var(--lumo-font-size-l); - line-height: 1; - height: 25px; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 10px 10px; - background-color: var(--lumo-contrast-5pct); - border-bottom: 1px solid var(--lumo-contrast-10pct); + gap: 10px; } - + .configHeader { padding: 10px; } @@ -78,6 +61,12 @@ export class QwcDevServices extends LitElement { this._services = devServices; } + hotReload(){ + import(`devui/devui-data.js?${Date.now()}`).then(newDevUIData => { + this._services = newDevUIData.devServices; + }); + } + render() { if (this._services && this._services.length>0) { return html`
    @@ -93,11 +82,12 @@ export class QwcDevServices extends LitElement { } _renderCard(devService){ - return html`
    -
    ${devService.name}
    - ${this._renderContainerDetails(devService)} - ${this._renderConfigDetails(devService)} -
    `; + return html` +
    + ${this._renderContainerDetails(devService)} + ${this._renderConfigDetails(devService)} +
    +
    `; } _renderContainerDetails(devService){ diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js index 37ff40a0a803e..b7f8b7ee8834b 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js @@ -1,4 +1,4 @@ -import { LitElement, html, css} from 'lit'; +import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element'; import { JsonRpc } from 'jsonrpc'; import '@vaadin/icon'; import 'qui-badge'; @@ -6,7 +6,7 @@ import 'qui-badge'; /** * This component adds a custom link on the Extension card */ -export class QwcExtensionLink extends LitElement { +export class QwcExtensionLink extends QwcHotReloadElement { static styles = css` .extensionLink { @@ -49,9 +49,86 @@ export class QwcExtensionLink extends LitElement { _effectiveLabel: {state: true}, _observer: {state: false}, }; + + _staticLabel = null; + _dynamicLabel = null; + _streamingLabel = null; + + set staticLabel(val) { + if(!this._staticLabel || (this._staticLabel && this._staticLabel != val)){ + let oldVal = this._staticLabel; + this._staticLabel = val; + this.requestUpdate('staticLabel', oldVal); + this.hotReload(); + } + } + + get staticLabel() { + return this._staticLabel; + } + + set dynamicLabel(val) { + if(!this._dynamicLabel || (this._dynamicLabel && this._dynamicLabel != val)){ + let oldVal = this._dynamicLabel; + this._dynamicLabel = val; + this.requestUpdate('dynamicLabel', oldVal); + this.hotReload(); + } + } + + get dynamicLabel() { + return this._dynamicLabel; + } + + set streamingLabel(val) { + if(!this._streamingLabel || (this._streamingLabel && this._streamingLabel != val)){ + let oldVal = this._streamingLabel; + this._streamingLabel = val; + this.requestUpdate('streamingLabel', oldVal); + this.hotReload(); + } + } + + get streamingLabel() { + return this._streamingLabel; + } connectedCallback() { super.connectedCallback(); + this.hotReload(); + } + + hotReload(){ + if(this._observer){ + this._observer.cancel(); + } + this._effectiveLabel = null; + if(this.streamingLabel){ + this.jsonRpc = new JsonRpc(this); + this._observer = this.jsonRpc[this.streamingLabel]().onNext(jsonRpcResponse => { + let oldVal = this._effectiveLabel; + this._effectiveLabel = jsonRpcResponse.result; + this.requestUpdate('_effectiveLabel', oldVal); + }); + }else if(this.dynamicLabel){ + this.jsonRpc = new JsonRpc(this); + this.jsonRpc[this.dynamicLabel]().then(jsonRpcResponse => { + let oldVal = this._effectiveLabel; + this._effectiveLabel = jsonRpcResponse.result; + this.requestUpdate('_effectiveLabel', oldVal); + }); + }else if(this.staticLabel){ + let oldVal = this._effectiveLabel; + this._effectiveLabel = this.staticLabel; + this.requestUpdate('_effectiveLabel', oldVal); + }else{ + let oldVal = this._effectiveLabel; + this._effectiveLabel = null; + this.requestUpdate('_effectiveLabel', oldVal); + } + } + + _getEffectiveLabel(){ if(this.streamingLabel){ this.jsonRpc = new JsonRpc(this); this._observer = this.jsonRpc[this.streamingLabel]().onNext(jsonRpcResponse => { @@ -71,28 +148,30 @@ export class QwcExtensionLink extends LitElement { if(this._observer){ this._observer.cancel(); } - super.disconnectedCallback() + super.disconnectedCallback(); } render() { - let routerIgnore = false; - - let p = this.path; - let t = "_self"; - if(!this.embed){ - routerIgnore = true; - p = this.externalUrl; - t = "_blank"; + if(this.path){ + let routerIgnore = false; + + let p = this.path; + let t = "_self"; + if(!this.embed){ + routerIgnore = true; + p = this.externalUrl; + t = "_blank"; + } + return html` + + + + ${this.displayName} + + ${this._renderBadge()} + + `; } - return html` - - - - ${this.displayName} - - ${this._renderBadge()} - - `; } _renderBadge() { diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js index 0da0c18e84e41..931ad190ec6be 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js @@ -44,6 +44,10 @@ export class QwcExtension extends LitElement { visibility:hidden; } + .card-footer a { + color: var(--lumo-contrast-50pct); + } + .active:hover { box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); } @@ -137,7 +141,9 @@ export class QwcExtension extends LitElement { _footerTemplate() { return html` `; diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-header.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-header.js index 6f4d46f4a6cb2..720e89e91b5f2 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-header.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-header.js @@ -8,6 +8,7 @@ import { devuiState } from 'devui-state'; import '@vaadin/menu-bar'; import '@vaadin/tabs'; import '@vaadin/button'; +import 'qwc/qwc-extension-link.js'; /** * This component represent the Dev UI Header @@ -236,7 +237,9 @@ export class QwcHeader extends observeState(LitElement) { if(subMenu){ this._rightSideNav = html` ${subMenu.links.map(link => - html`${link.name}` + html` + ${this._renderSubMenuLink(link)} + ` )} `; }else{ @@ -248,6 +251,25 @@ export class QwcHeader extends observeState(LitElement) { } } + _renderSubMenuLink(link){ + + let relativePath = link.page.id.replace(link.page.namespace + "/", ""); + + return html` + `; + } + _reload(e) { fetch(devuiState.applicationInfo.contextRoot).then(response => { this.routerController.goHome(); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js index 2396d0ee7c2a7..9f1d10ffe35cc 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js @@ -5,10 +5,16 @@ import { JsonRpc } from 'jsonrpc'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import '@vaadin/icon'; import '@vaadin/dialog'; +import '@vaadin/select'; import '@vaadin/checkbox'; import '@vaadin/checkbox-group'; import { dialogHeaderRenderer, dialogRenderer } from '@vaadin/dialog/lit.js'; import 'qui-badge'; +import '@vaadin/grid'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; +import '@vaadin/vertical-layout'; +import { loggerLevels } from 'devui-data'; /** * This component represent the Server Log @@ -73,13 +79,6 @@ export class QwcServerLog extends QwcHotReloadElement { color: var(--lumo-success-color-50pct); } - .columnsDialog{ - - } - - .levelsDialog{ - - } `; static properties = { @@ -91,10 +90,22 @@ export class QwcServerLog extends QwcHotReloadElement { _levelsDialogOpened: {state: true}, _columnsDialogOpened: {state: true}, _selectedColumns: {state: true}, + _allLoggers: {state: true, type: Array}, + _filteredLoggers: {state: true, type: Array}, + _loggerLevels: {state: false, type: Array} }; constructor() { super(); + this._loggerLevels = []; + for (let i in loggerLevels) { + let loggerLevel = loggerLevels[i]; + this._loggerLevels.push({ + 'label': loggerLevel, + 'value': loggerLevel, + }); + } + this.logControl .addToggle("On/off switch", true, (e) => { this._toggleOnOffClicked(e); @@ -129,6 +140,7 @@ export class QwcServerLog extends QwcHotReloadElement { this._addLogEntry(entry); }); }); + this._loadAllLoggers(); } disconnectedCallback() { @@ -136,11 +148,20 @@ export class QwcServerLog extends QwcHotReloadElement { this._toggleOnOff(false); } + _loadAllLoggers(){ + this.jsonRpc.getLoggers().then(jsonRpcResponse => { + this._allLoggers = jsonRpcResponse.result; + this._filteredLoggers = this._allLoggers; + }); + } + render() { - - return html` + if(this._filteredLoggers){ + return html` html` @@ -154,6 +175,8 @@ export class QwcServerLog extends QwcHotReloadElement { > html` @@ -176,7 +199,7 @@ export class QwcServerLog extends QwcHotReloadElement { ` )} `; - + } } _renderLogEntry(message){ @@ -379,11 +402,80 @@ export class QwcServerLog extends QwcHotReloadElement { } _renderLevelsDialog(){ - return html` - Hello levels - `; + if(this._filteredLoggers){ + return html` + + + + + + + + + + + + + `; + } } + _filterLoggers(e) { + const searchTerm = (e.detail.value || '').trim(); + if (searchTerm === '') { + this._filteredLoggers = this._allLoggers; + return; + } + + this._filteredLoggers = this._allLoggers.filter((level) => { + let i = this._matchLogger(level.name, searchTerm); + return i; + }); + } + + _matchLogger(value, term) { + if (!value) { + return false; + } + return value.toLowerCase().includes(term.toLowerCase()); + } + + _logLevelRenderer(logger){ + return html`${this._renderSelect(logger.name, logger.effectiveLevel)}`; + } + + _renderSelect(loggerName, loggerLevel){ + return html` + `; + } + + _logLevelSelectChanged(event){ + let name = event.target.id; + this._updateLogLevel(name, event.target.value); + } + + _updateLogLevel(name, value){ + this.jsonRpc.updateLogLevel({ + 'loggerName': name, + 'levelValue': value + }); + } + _renderColumnsDialog(){ return html` streamLog() { return logStreamBroadcaster.getLogStream(); } + @NonBlocking + public List getLoggers() { + LogContext logContext = LogContext.getLogContext(); + List values = new ArrayList<>(); + Enumeration loggerNames = logContext.getLoggerNames(); + while (loggerNames.hasMoreElements()) { + String loggerName = loggerNames.nextElement(); + JsonObject jsonObject = getLogger(loggerName); + if (jsonObject != null) { + values.add(jsonObject); + } + } + return values; + } + + @NonBlocking + public JsonObject getLogger(String loggerName) { + LogContext logContext = LogContext.getLogContext(); + if (loggerName != null && !loggerName.isEmpty()) { + Logger logger = logContext.getLogger(loggerName); + return JsonObject.of( + "name", loggerName, + "effectiveLevel", getEffectiveLogLevel(logger), + "configuredLevel", getConfiguredLogLevel(logger)); + } + return null; + } + + @NonBlocking + public JsonObject updateLogLevel(String loggerName, String levelValue) { + LogContext logContext = LogContext.getLogContext(); + Logger logger = logContext.getLogger(loggerName); + java.util.logging.Level level; + if (levelValue == null || levelValue.isBlank()) { + if (logger.getParent() != null) { + level = logger.getParent().getLevel(); + } else { + throw new IllegalArgumentException("The level of the root logger cannot be set to null"); + } + } else { + level = Level.parse(levelValue); + } + logger.setLevel(level); + LOG.info("Log level updated [" + loggerName + "] changed to [" + levelValue + "]"); + + return getLogger(loggerName); + } + + private String getConfiguredLogLevel(Logger logger) { + java.util.logging.Level level = logger.getLevel(); + return level != null ? level.getName() : null; + } + + private String getEffectiveLogLevel(Logger logger) { + if (logger == null) { + return null; + } + if (logger.getLevel() != null) { + return logger.getLevel().getName(); + } + return getEffectiveLogLevel(logger.getParent()); + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java index a6a841e585b47..7738d9bfe34cd 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java @@ -113,6 +113,7 @@ public void accept(Route route) { // No other HTTP methods should be used route.method(HttpMethod.GET); route.method(HttpMethod.HEAD); + route.method(HttpMethod.OPTIONS); for (Handler i : handlers) { route.handler(i); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/AbstractResponseWrapper.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/AbstractResponseWrapper.java index d64a3311bfcd9..959af552f0dc7 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/AbstractResponseWrapper.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/AbstractResponseWrapper.java @@ -198,6 +198,16 @@ public HttpServerResponse writeContinue() { return this; } + @Override + public Future writeEarlyHints(MultiMap headers) { + return delegate.writeEarlyHints(headers); + } + + @Override + public void writeEarlyHints(MultiMap headers, Handler> handler) { + delegate.writeEarlyHints(headers, handler); + } + @Override public Future end(String chunk) { return delegate.end(chunk); diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/graal/VertxSubstitutions.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/graal/VertxSubstitutions.java index 0a7313ca0473b..14b5ef952116c 100644 --- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/graal/VertxSubstitutions.java +++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/core/runtime/graal/VertxSubstitutions.java @@ -30,15 +30,16 @@ import io.vertx.core.impl.ContextInternal; import io.vertx.core.impl.VertxInternal; import io.vertx.core.impl.resolver.DefaultResolverProvider; +import io.vertx.core.impl.transports.JDKTransport; import io.vertx.core.net.NetServerOptions; -import io.vertx.core.net.impl.transport.Transport; import io.vertx.core.spi.resolver.ResolverProvider; +import io.vertx.core.spi.transport.Transport; -@TargetClass(className = "io.vertx.core.net.impl.transport.Transport") -final class Target_io_vertx_core_net_impl_transport_Transport { +@TargetClass(className = "io.vertx.core.impl.VertxBuilder") +final class Target_io_vertx_core_impl_VertxBuilder { @Substitute public static Transport nativeTransport() { - return Transport.JDK; + return JDKTransport.INSTANCE; } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index 215ce1e4c4e08..7e9457dd05743 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -511,6 +511,12 @@ Map> getQualifierNonbindingMembers() { return qualifierNonbindingMembers; } + /** + * + * @return the collection of interceptor bindings; the container annotations of repeating interceptor binding are not + * included + * @see #extractInterceptorBindings(AnnotationInstance) + */ public Collection getInterceptorBindings() { return Collections.unmodifiableCollection(interceptorBindings.values()); } @@ -614,7 +620,7 @@ Collection extractQualifiers(AnnotationInstance annotation) * @param annotation annotation to be inspected * @return a collection of interceptor bindings or an empty collection */ - Collection extractInterceptorBindings(AnnotationInstance annotation) { + public Collection extractInterceptorBindings(AnnotationInstance annotation) { return extractAnnotations(annotation, interceptorBindings, repeatingInterceptorBindingAnnotations); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinScope.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinScope.java index d664b561671e1..29ade50172ec4 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinScope.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinScope.java @@ -43,7 +43,7 @@ public static BuiltinScope from(DotName scopeAnnotationName) { public static BuiltinScope from(ClassInfo clazz) { for (BuiltinScope scope : BuiltinScope.values()) { - if (clazz.classAnnotation(scope.getName()) != null) { + if (clazz.hasDeclaredAnnotation(scope.getName())) { return scope; } } @@ -69,7 +69,7 @@ public static boolean isIn(Iterable annotations) { public static boolean isDeclaredOn(ClassInfo clazz) { for (BuiltinScope scope : BuiltinScope.values()) { - if (clazz.classAnnotation(scope.getName()) != null) { + if (clazz.hasDeclaredAnnotation(scope.getName())) { return true; } } 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 440a8d6f5f238..8b653f334f3c5 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 @@ -39,19 +39,19 @@ public class QuarkusClassLoader extends ClassLoader implements Closeable { registerAsParallelCapable(); } - public static List getElements(String resourceName, boolean localOnly) { + public static List getElements(String resourceName, boolean onlyFromCurrentClassLoader) { final ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (!(ccl instanceof QuarkusClassLoader)) { throw new IllegalStateException("The current classloader is not an instance of " + QuarkusClassLoader.class.getName() + " but " + ccl.getClass().getName()); } - return ((QuarkusClassLoader) ccl).getElementsWithResource(resourceName, localOnly); + return ((QuarkusClassLoader) ccl).getElementsWithResource(resourceName, onlyFromCurrentClassLoader); } - public List getAllElements(boolean localOnly) { + public List getAllElements(boolean onlyFromCurrentClassLoader) { List ret = new ArrayList<>(); - if (parent instanceof QuarkusClassLoader && !localOnly) { - ret.addAll(((QuarkusClassLoader) parent).getAllElements(localOnly)); + if (parent instanceof QuarkusClassLoader && !onlyFromCurrentClassLoader) { + ret.addAll(((QuarkusClassLoader) parent).getAllElements(onlyFromCurrentClassLoader)); } ret.addAll(elements); return ret; diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java index 0cab46d8b25c3..d3772143e0d8e 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java @@ -153,7 +153,11 @@ private LocalProject loadAndCacheProject(Path pomFile) throws BootstrapMavenExce } private Model rawModel(Path pomFile) throws BootstrapMavenException { - final Path moduleDir = pomFile.getParent(); + var moduleDir = pomFile.getParent(); + if (moduleDir != null) { + // the path might not be normalized, while the modelProvider below would typically recognize normalized absolute paths + moduleDir = moduleDir.normalize().toAbsolutePath(); + } Model rawModel = rawModelCache.get(moduleDir); if (rawModel != null) { return rawModel; diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamItem.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamItem.java index 4a31a0346ef47..35abc4465f858 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamItem.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamItem.java @@ -6,6 +6,12 @@ public class BeanParamItem extends Item { private final List items; private final String className; + public BeanParamItem(String fieldName, List items, String className, ValueExtractor extractor) { + super(fieldName, ItemType.BEAN_PARAM, extractor); + this.items = items; + this.className = className; + } + public String className() { return className; } @@ -13,10 +19,4 @@ public String className() { public List items() { return items; } - - public BeanParamItem(List items, String className, ValueExtractor extractor) { - super(ItemType.BEAN_PARAM, extractor); - this.items = items; - this.className = className; - } } diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java index d56ce79303f9a..237de1c023670 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java @@ -61,18 +61,19 @@ private static List parseInternal(ClassInfo beanParamClass, IndexView inde } resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, QUERY_PARAM, - (annotationValue, fieldInfo) -> new QueryParamItem(annotationValue, + (annotationValue, fieldInfo) -> new QueryParamItem(fieldInfo.name(), annotationValue, new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()), fieldInfo.type()), - (annotationValue, getterMethod) -> new QueryParamItem(annotationValue, new GetterExtractor(getterMethod), + (annotationValue, getterMethod) -> new QueryParamItem(getterMethod.name(), annotationValue, + new GetterExtractor(getterMethod), getterMethod.returnType()))); resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, REST_QUERY_PARAM, - (annotationValue, fieldInfo) -> new QueryParamItem( + (annotationValue, fieldInfo) -> new QueryParamItem(fieldInfo.name(), annotationValue != null ? annotationValue : fieldInfo.name(), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()), fieldInfo.type()), - (annotationValue, getterMethod) -> new QueryParamItem( + (annotationValue, getterMethod) -> new QueryParamItem(getterMethod.name(), annotationValue != null ? annotationValue : getterName(getterMethod), new GetterExtractor(getterMethod), getterMethod.returnType()))); @@ -84,7 +85,7 @@ private static List parseInternal(ClassInfo beanParamClass, IndexView inde DotName beanParamClassName = type.asClassType().name(); List subBeanParamItems = parseInternal(index.getClassByName(beanParamClassName), index, processedBeanParamClasses); - return new BeanParamItem(subBeanParamItems, beanParamClassName.toString(), + return new BeanParamItem(fieldInfo.name(), subBeanParamItems, beanParamClassName.toString(), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())); } else { throw new IllegalArgumentException("BeanParam annotation used on a field that is not an object: " @@ -95,69 +96,71 @@ private static List parseInternal(ClassInfo beanParamClass, IndexView inde Type returnType = getterMethod.returnType(); List items = parseInternal(index.getClassByName(returnType.name()), index, processedBeanParamClasses); - return new BeanParamItem(items, beanParamClass.name().toString(), new GetterExtractor(getterMethod)); + return new BeanParamItem(getterMethod.name(), items, beanParamClass.name().toString(), + new GetterExtractor(getterMethod)); })); resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, COOKIE_PARAM, - (annotationValue, fieldInfo) -> new CookieParamItem(annotationValue, + (annotationValue, fieldInfo) -> new CookieParamItem(fieldInfo.name(), annotationValue, new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()), fieldInfo.type().name().toString()), - (annotationValue, getterMethod) -> new CookieParamItem(annotationValue, + (annotationValue, getterMethod) -> new CookieParamItem(getterMethod.name(), annotationValue, new GetterExtractor(getterMethod), getterMethod.returnType().name().toString()))); resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, REST_COOKIE_PARAM, - (annotationValue, fieldInfo) -> new CookieParamItem( + (annotationValue, fieldInfo) -> new CookieParamItem(fieldInfo.name(), annotationValue != null ? annotationValue : fieldInfo.name(), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()), fieldInfo.type().name().toString()), - (annotationValue, getterMethod) -> new CookieParamItem( + (annotationValue, getterMethod) -> new CookieParamItem(getterMethod.name(), annotationValue != null ? annotationValue : getterName(getterMethod), new GetterExtractor(getterMethod), getterMethod.returnType().name().toString()))); resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, HEADER_PARAM, - (annotationValue, fieldInfo) -> new HeaderParamItem(annotationValue, + (annotationValue, fieldInfo) -> new HeaderParamItem(fieldInfo.name(), annotationValue, new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()), fieldInfo.type().name().toString()), - (annotationValue, getterMethod) -> new HeaderParamItem(annotationValue, + (annotationValue, getterMethod) -> new HeaderParamItem(getterMethod.name(), annotationValue, new GetterExtractor(getterMethod), getterMethod.returnType().name().toString()))); // @RestHeader with no explicit value are hyphenated resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, REST_HEADER_PARAM, - (annotationValue, fieldInfo) -> new HeaderParamItem( + (annotationValue, fieldInfo) -> new HeaderParamItem(fieldInfo.name(), annotationValue != null ? annotationValue : StringUtil.hyphenateWithCapitalFirstLetter(fieldInfo.name()), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()), fieldInfo.type().name().toString()), - (annotationValue, getterMethod) -> new HeaderParamItem( + (annotationValue, getterMethod) -> new HeaderParamItem(getterMethod.name(), annotationValue != null ? annotationValue : StringUtil.hyphenateWithCapitalFirstLetter(getterName(getterMethod)), new GetterExtractor(getterMethod), getterMethod.returnType().name().toString()))); resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, PATH_PARAM, - (annotationValue, fieldInfo) -> new PathParamItem(annotationValue, fieldInfo.type().name().toString(), + (annotationValue, fieldInfo) -> new PathParamItem(fieldInfo.name(), annotationValue, + fieldInfo.type().name().toString(), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())), - (annotationValue, getterMethod) -> new PathParamItem(annotationValue, + (annotationValue, getterMethod) -> new PathParamItem(getterMethod.name(), annotationValue, getterMethod.returnType().name().toString(), new GetterExtractor(getterMethod)))); resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, REST_PATH_PARAM, - (annotationValue, fieldInfo) -> new PathParamItem( + (annotationValue, fieldInfo) -> new PathParamItem(fieldInfo.name(), annotationValue != null ? annotationValue : fieldInfo.name(), fieldInfo.type().name().toString(), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())), - (annotationValue, getterMethod) -> new PathParamItem( + (annotationValue, getterMethod) -> new PathParamItem(getterMethod.name(), annotationValue != null ? annotationValue : getterName(getterMethod), getterMethod.returnType().name().toString(), new GetterExtractor(getterMethod)))); resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, FORM_PARAM, - (annotationValue, fieldInfo) -> new FormParamItem(annotationValue, + (annotationValue, fieldInfo) -> new FormParamItem(fieldInfo.name(), annotationValue, fieldInfo.type().name().toString(), AsmUtil.getSignature(fieldInfo.type(), arg -> arg), fieldInfo.name(), partType(fieldInfo), fileName(fieldInfo), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())), - (annotationValue, getterMethod) -> new FormParamItem(annotationValue, + (annotationValue, getterMethod) -> new FormParamItem(getterMethod.name(), annotationValue, getterMethod.returnType().name().toString(), AsmUtil.getSignature(getterMethod.returnType(), arg -> arg), getterMethod.name(), @@ -165,13 +168,13 @@ private static List parseInternal(ClassInfo beanParamClass, IndexView inde new GetterExtractor(getterMethod)))); resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, REST_FORM_PARAM, - (annotationValue, fieldInfo) -> new FormParamItem( + (annotationValue, fieldInfo) -> new FormParamItem(fieldInfo.name(), annotationValue != null ? annotationValue : fieldInfo.name(), fieldInfo.type().name().toString(), AsmUtil.getSignature(fieldInfo.type(), arg -> arg), fieldInfo.name(), partType(fieldInfo), fileName(fieldInfo), new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())), - (annotationValue, getterMethod) -> new FormParamItem( + (annotationValue, getterMethod) -> new FormParamItem(getterMethod.name(), annotationValue != null ? annotationValue : getterName(getterMethod), getterMethod.returnType().name().toString(), AsmUtil.getSignature(getterMethod.returnType(), arg -> arg), diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/CookieParamItem.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/CookieParamItem.java index c865b7dc475e9..ba068e0ad463f 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/CookieParamItem.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/CookieParamItem.java @@ -4,8 +4,8 @@ public class CookieParamItem extends Item { private final String cookieName; private final String paramType; - public CookieParamItem(String cookieName, ValueExtractor extractor, String paramType) { - super(ItemType.COOKIE, extractor); + public CookieParamItem(String fieldName, String cookieName, ValueExtractor extractor, String paramType) { + super(fieldName, ItemType.COOKIE, extractor); this.cookieName = cookieName; this.paramType = paramType; } diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java index ebf9f08ccad23..60c2fe9d3ccb0 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java @@ -9,11 +9,11 @@ public class FormParamItem extends Item { private final String fileName; private final String sourceName; - public FormParamItem(String formParamName, String paramType, String paramSignature, + public FormParamItem(String fieldName, String formParamName, String paramType, String paramSignature, String sourceName, String mimeType, String fileName, ValueExtractor valueExtractor) { - super(ItemType.FORM_PARAM, valueExtractor); + super(fieldName, ItemType.FORM_PARAM, valueExtractor); this.formParamName = formParamName; this.paramType = paramType; this.paramSignature = paramSignature; diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/HeaderParamItem.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/HeaderParamItem.java index b47110b960d4b..61b417b3865ff 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/HeaderParamItem.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/HeaderParamItem.java @@ -4,8 +4,8 @@ public class HeaderParamItem extends Item { private final String headerName; private final String paramType; - public HeaderParamItem(String headerName, ValueExtractor extractor, String paramType) { - super(ItemType.HEADER_PARAM, extractor); + public HeaderParamItem(String fieldName, String headerName, ValueExtractor extractor, String paramType) { + super(fieldName, ItemType.HEADER_PARAM, extractor); this.headerName = headerName; this.paramType = paramType; } diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/Item.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/Item.java index 4bda26033130d..1ef10511912ad 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/Item.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/Item.java @@ -5,14 +5,20 @@ public abstract class Item { + private final String fieldName; private final ItemType type; private final ValueExtractor valueExtractor; - public Item(ItemType type, ValueExtractor valueExtractor) { + public Item(String fieldName, ItemType type, ValueExtractor valueExtractor) { + this.fieldName = fieldName; this.type = type; this.valueExtractor = valueExtractor; } + public String fieldName() { + return fieldName; + } + public ItemType type() { return type; } diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/PathParamItem.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/PathParamItem.java index 4db45bd16baaf..474052a4f0a39 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/PathParamItem.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/PathParamItem.java @@ -5,8 +5,8 @@ public class PathParamItem extends Item { private final String pathParamName; private final String paramType; - public PathParamItem(String pathParamName, String paramType, ValueExtractor valueExtractor) { - super(ItemType.PATH_PARAM, valueExtractor); + public PathParamItem(String fieldName, String pathParamName, String paramType, ValueExtractor valueExtractor) { + super(fieldName, ItemType.PATH_PARAM, valueExtractor); this.pathParamName = pathParamName; this.paramType = paramType; } diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/QueryParamItem.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/QueryParamItem.java index 8fb3cc58a2e0f..a3b83a979d817 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/QueryParamItem.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/QueryParamItem.java @@ -7,8 +7,8 @@ public class QueryParamItem extends Item { private final String name; private final Type valueType; - public QueryParamItem(String name, ValueExtractor extractor, Type valueType) { - super(ItemType.QUERY_PARAM, extractor); + public QueryParamItem(String fieldName, String name, ValueExtractor extractor, Type valueType) { + super(fieldName, ItemType.QUERY_PARAM, extractor); this.name = name; this.valueType = valueType; } diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java index e390280be4816..eaba90c22749c 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java @@ -8,6 +8,7 @@ import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.JSONP_JSON_STRUCTURE; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.JSONP_JSON_VALUE; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -44,6 +45,8 @@ public class ClientEndpointIndexer extends EndpointIndexer { static final DotName CONTINUATION = DotName.createSimple("kotlin.coroutines.Continuation"); + static final DotName CLIENT_EXCEPTION_MAPPER = DotName + .createSimple("io.quarkus.rest.client.reactive.ClientExceptionMapper"); private final String[] defaultProduces; private final String[] defaultProducesNegotiated; @@ -86,8 +89,19 @@ public MaybeRestClientInterface createClientProxy(ClassInfo classInfo, } private void warnForUnsupportedAnnotations(ClassInfo classInfo) { - if ((classInfo.annotationsMap().get(ResteasyReactiveDotNames.BLOCKING) != null) - || (classInfo.annotationsMap().get(ResteasyReactiveDotNames.NON_BLOCKING) != null)) { + List offendingBlockingAnnotations = new ArrayList<>(); + + List blockingAnnotations = classInfo.annotationsMap().get(ResteasyReactiveDotNames.BLOCKING); + if (blockingAnnotations != null) { + for (AnnotationInstance blockingAnnotation : blockingAnnotations) { + // If the `@Blocking` annotation is used along with the `@ClientExceptionMapper`, we support it. + if (blockingAnnotation.target().annotation(CLIENT_EXCEPTION_MAPPER) == null) { + offendingBlockingAnnotations.add(blockingAnnotation); + } + } + } + if (!offendingBlockingAnnotations.isEmpty() + || classInfo.annotationsMap().get(ResteasyReactiveDotNames.NON_BLOCKING) != null) { log.warn( "'@Blocking' and '@NonBlocking' annotations are not necessary (or supported) on REST Client interfaces. Offending class is '" + classInfo.name() diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java index 30df0dde5d5f5..2e534c08d8cd0 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java @@ -58,6 +58,7 @@ import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpClosedException; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpVersion; import io.vertx.core.http.RequestOptions; @@ -375,7 +376,14 @@ public void handle(Buffer buffer) { .onFailure(new Handler<>() { @Override public void handle(Throwable failure) { - if (failure instanceof IOException) { + if (failure instanceof HttpClosedException) { + // This is because of the Rest Client TCK + // HttpClosedException is a runtime exception. If we complete with that exception, it gets + // unwrapped by the rest client proxy and thus fails the TCK. + // By creating an IOException, we avoid that and provide a meaningful exception (because + // it's an I/O exception) + requestContext.resume(new ProcessingException(new IOException(failure.getMessage()))); + } else if (failure instanceof IOException) { requestContext.resume(new ProcessingException(failure)); } else { requestContext.resume(failure); diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java index e96e589754c26..476a8f662b887 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java @@ -74,6 +74,7 @@ import io.vertx.core.net.NetClientOptions; import io.vertx.core.net.NetServer; import io.vertx.core.net.NetServerOptions; +import io.vertx.core.net.SSLOptions; import io.vertx.core.shareddata.SharedData; import io.vertx.core.spi.VerticleFactory; @@ -698,6 +699,11 @@ public boolean isNativeTransportEnabled() { return getDelegate().isNativeTransportEnabled(); } + @Override + public Throwable unavailableNativeTransportCause() { + return getDelegate().unavailableNativeTransportCause(); + } + @Override public Vertx exceptionHandler(Handler handler) { return getDelegate().exceptionHandler(handler); @@ -829,6 +835,11 @@ public Future webSocketAbs(String url, MultiMap headers, WebsocketVer return getDelegate().webSocketAbs(url, headers, version, subProtocols); } + @Override + public Future updateSSLOptions(SSLOptions options) { + return getDelegate().updateSSLOptions(options); + } + @Override public HttpClient connectionHandler(Handler handler) { return getDelegate().connectionHandler(handler); diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java index 1d0e1380b3cfc..59e89940f428c 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java @@ -179,7 +179,9 @@ protected Throwable unwrapException(Throwable t) { + invokedMethod.getDeclaringClass().getName() + "#" + invokedMethod.getName() + "'"; } - return new ClientWebApplicationException(message, webApplicationException, + return new ClientWebApplicationException(message, + webApplicationException instanceof ClientWebApplicationException ? webApplicationException.getCause() + : webApplicationException, webApplicationException.getResponse()); } return res; diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index 4ee87d8d3173d..15c6ce6814157 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -219,7 +219,7 @@ public abstract class EndpointIndexer> classLevelExceptionMappers; private final Function> factoryCreator; - private final Consumer resourceMethodCallback; + private final Consumer resourceMethodCallback; private final AnnotationStore annotationStore; protected final ApplicationScanningResult applicationScanningResult; private final Set contextTypes; @@ -773,7 +773,9 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf handleAdditionalMethodProcessing((METHOD) method, currentClassInfo, currentMethodInfo, getAnnotationStore()); if (resourceMethodCallback != null) { resourceMethodCallback.accept( - new ResourceMethodCallbackData(basicResourceClassInfo, actualEndpointInfo, currentMethodInfo, method)); + new ResourceMethodCallbackEntry(this, index, basicResourceClassInfo, actualEndpointInfo, + currentMethodInfo, + method)); } return method; } catch (Exception e) { @@ -923,6 +925,10 @@ protected void handleAdditionalMethodProcessing(METHOD method, ClassInfo current } + public boolean additionalRegisterClassForReflectionCheck(ResourceMethodCallbackEntry entry) { + return false; + } + protected abstract InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, Map existingConverters, @@ -1581,7 +1587,7 @@ public static abstract class Builder, B private AdditionalWriters additionalWriters; private boolean hasRuntimeConverters; private Map> classLevelExceptionMappers; - private Consumer resourceMethodCallback; + private Consumer resourceMethodCallback; private Collection annotationsTransformers; private ApplicationScanningResult applicationScanningResult; private final Set contextTypes = new HashSet<>(DEFAULT_CONTEXT_TYPES); @@ -1689,7 +1695,7 @@ public B setClassLevelExceptionMappers(Map> classLe return (B) this; } - public B setResourceMethodCallback(Consumer resourceMethodCallback) { + public B setResourceMethodCallback(Consumer resourceMethodCallback) { this.resourceMethodCallback = resourceMethodCallback; return (B) this; } @@ -1761,14 +1767,22 @@ public String getStreamElementType() { } } - public static class ResourceMethodCallbackData { + @SuppressWarnings("rawtypes") + public static class ResourceMethodCallbackEntry { + + private final EndpointIndexer indexer; + private final IndexView index; private final BasicResourceClassInfo basicResourceClassInfo; private final ClassInfo actualEndpointInfo; private final MethodInfo methodInfo; private final ResourceMethod resourceMethod; - public ResourceMethodCallbackData(BasicResourceClassInfo basicResourceClassInfo, ClassInfo actualEndpointInfo, + public ResourceMethodCallbackEntry(EndpointIndexer indexer, IndexView index, + BasicResourceClassInfo basicResourceClassInfo, + ClassInfo actualEndpointInfo, MethodInfo methodInfo, ResourceMethod resourceMethod) { + this.indexer = indexer; + this.index = index; this.basicResourceClassInfo = basicResourceClassInfo; this.methodInfo = methodInfo; this.actualEndpointInfo = actualEndpointInfo; @@ -1790,6 +1804,10 @@ public ClassInfo getActualEndpointInfo() { public ResourceMethod getResourceMethod() { return resourceMethod; } + + public boolean additionalRegisterClassForReflectionCheck() { + return indexer.additionalRegisterClassForReflectionCheck(this); + } } public static class DeclaredTypes { diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/JandexUtil.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/JandexUtil.java index 1e2f2f6fc29ad..fbc85b77b380e 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/JandexUtil.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/JandexUtil.java @@ -364,4 +364,39 @@ public static boolean isSubclassOf(IndexView index, ClassInfo info, DotName pare return isSubclassOf(index, superClass, parentName); } + /** + * Returns true if the given Jandex ClassInfo is a subclass of or inherits the given name. + * + * @param index the index to use to look up super classes. + * @param info the ClassInfo we want to check. + * @param name the name of the superclass or interface we want to find. + * @throws RuntimeException if one of the superclasses is not indexed. + */ + public static boolean isImplementorOf(IndexView index, ClassInfo info, DotName name) { + // Check interfaces + List interfaceNames = info.interfaceNames(); + for (DotName interfaceName : interfaceNames) { + if (interfaceName.equals(name)) { + return true; + } + } + + // Check direct hierarchy + if (info.superName().equals(DOTNAME_OBJECT) || info.superName().equals(DOTNAME_RECORD)) { + return false; + } + if (info.superName().equals(name)) { + return true; + } + + // climb up the hierarchy of classes + Type superType = info.superClassType(); + ClassInfo superClass = index.getClassByName(superType.name()); + if (superClass == null) { + // this can happens if the parent is not inside the Jandex index + throw new RuntimeException("The class " + superType.name() + " is not inside the Jandex index"); + } + return isImplementorOf(index, superClass, name); + } + } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java index 2b757040a0e11..5598c9ef65087 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java @@ -153,7 +153,6 @@ public static SerializerScanningResult scanForSerializers(IndexView index, runtimeType = RuntimeType.SERVER; } List mediaTypeStrings = Collections.emptyList(); - String readerClassName = readerClass.name().toString(); AnnotationInstance consumesAnnotation = readerClass.classAnnotation(ResteasyReactiveDotNames.CONSUMES); if (consumesAnnotation != null) { mediaTypeStrings = Arrays.asList(consumesAnnotation.value().asStringArray()); @@ -167,7 +166,7 @@ public static SerializerScanningResult scanForSerializers(IndexView index, if (priorityInstance != null) { priority = priorityInstance.value().asInt(); } - readerList.add(new ScannedSerializer(readerClassName, + readerList.add(new ScannedSerializer(readerClass, typeParameters.get(0).name().toString(), mediaTypeStrings, runtimeType, false, priority)); } } @@ -192,7 +191,6 @@ public static SerializerScanningResult scanForSerializers(IndexView index, List typeParameters = JandexUtil.resolveTypeParameters(writerClass.name(), ResteasyReactiveDotNames.MESSAGE_BODY_WRITER, index); - String writerClassName = writerClass.name().toString(); AnnotationInstance constrainedToInstance = writerClass.classAnnotation(ResteasyReactiveDotNames.CONSTRAINED_TO); if (constrainedToInstance != null) { runtimeType = RuntimeType.valueOf(constrainedToInstance.value().asEnum()); @@ -202,7 +200,7 @@ public static SerializerScanningResult scanForSerializers(IndexView index, if (priorityInstance != null) { priority = priorityInstance.value().asInt(); } - writerList.add(new ScannedSerializer(writerClassName, + writerList.add(new ScannedSerializer(writerClass, typeParameters.get(0).name().toString(), mediaTypeStrings, runtimeType, false, priority)); } } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ScannedSerializer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ScannedSerializer.java index 900513163de66..9df827b1531dc 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ScannedSerializer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ScannedSerializer.java @@ -5,8 +5,11 @@ import jakarta.ws.rs.Priorities; import jakarta.ws.rs.RuntimeType; +import org.jboss.jandex.ClassInfo; + public class ScannedSerializer { + private final ClassInfo classInfo; private final String className; private final String handledClassName; private final List mediaTypeStrings; @@ -14,12 +17,23 @@ public class ScannedSerializer { private final boolean builtin; private final Integer priority; + public ScannedSerializer(ClassInfo classInfo, String handledClassName, List mediaTypeStrings) { + this(classInfo, handledClassName, mediaTypeStrings, null, true, Priorities.USER); + } + + // used only for testing public ScannedSerializer(String className, String handledClassName, List mediaTypeStrings) { - this(className, handledClassName, mediaTypeStrings, null, true, Priorities.USER); + this(null, className, handledClassName, mediaTypeStrings, null, true, Priorities.USER); } - public ScannedSerializer(String className, String handledClassName, List mediaTypeStrings, + public ScannedSerializer(ClassInfo classInfo, String handledClassName, List mediaTypeStrings, RuntimeType runtimeType, boolean builtin, Integer priority) { + this(classInfo, classInfo.name().toString(), handledClassName, mediaTypeStrings, runtimeType, builtin, priority); + } + + private ScannedSerializer(ClassInfo classInfo, String className, String handledClassName, List mediaTypeStrings, + RuntimeType runtimeType, boolean builtin, Integer priority) { + this.classInfo = classInfo; this.className = className; this.handledClassName = handledClassName; this.mediaTypeStrings = mediaTypeStrings; @@ -28,6 +42,12 @@ public ScannedSerializer(String className, String handledClassName, List this.priority = priority; } + // used only for tests + + public ClassInfo getClassInfo() { + return classInfo; + } + public String getClassName() { return className; } diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 2c4a96a42e190..8752d4469f642 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -63,7 +63,7 @@ 1.6.8 2.1.0 2.1.0 - 4.3.8 + 4.4.1 5.3.0 1.0.0.Final 2.14.2 @@ -72,7 +72,7 @@ 3.0.3 3.0.0 4.2.0 - 3.2.0 + 3.3.0 1.0.4 1.0.0 diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/SseUtil.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/SseUtil.java index f0a3824f25c42..edef2f560405d 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/SseUtil.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/SseUtil.java @@ -16,7 +16,6 @@ import jakarta.ws.rs.sse.OutboundSseEvent; import jakarta.ws.rs.sse.SseEvent; -import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.util.CommonSseUtil; import org.jboss.resteasy.reactive.common.util.QuarkusMultivaluedHashMap; import org.jboss.resteasy.reactive.server.handlers.PublisherResponseHandler; @@ -135,10 +134,9 @@ private static String serialiseDataToString(ResteasyReactiveRequestContext conte ByteArrayOutputStream baos = new ByteArrayOutputStream(); boolean wrote = false; for (MessageBodyWriter writer : writers) { - // Spec(API) says we should use class/type/mediaType but doesn't talk about annotations - if (writer.isWriteable(entityClass, entityType, Serialisers.NO_ANNOTATION, mediaType)) { + if (writer.isWriteable(entityClass, entityType, context.getAllAnnotations(), mediaType)) { // FIXME: spec doesn't really say what headers we should use here - writer.writeTo(entity, entityClass, entityType, Serialisers.NO_ANNOTATION, mediaType, + writer.writeTo(entity, entityClass, entityType, context.getAllAnnotations(), mediaType, new QuarkusMultivaluedHashMap<>(), baos); wrote = true; break; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/StreamingUtil.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/StreamingUtil.java index 0903202a53402..8140b79ca2f39 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/StreamingUtil.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/StreamingUtil.java @@ -13,7 +13,6 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.MessageBodyWriter; -import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.util.QuarkusMultivaluedHashMap; import org.jboss.resteasy.reactive.server.StreamingOutputStream; import org.jboss.resteasy.reactive.server.handlers.PublisherResponseHandler; @@ -63,10 +62,9 @@ private static byte[] serialiseEntity(ResteasyReactiveRequestContext context, Ob StreamingOutputStream baos = new StreamingOutputStream(); boolean wrote = false; for (MessageBodyWriter writer : writers) { - // Spec(API) says we should use class/type/mediaType but doesn't talk about annotations - if (writer.isWriteable(entityClass, entityType, Serialisers.NO_ANNOTATION, mediaType)) { + if (writer.isWriteable(entityClass, entityType, context.getAllAnnotations(), mediaType)) { // FIXME: spec doesn't really say what headers we should use here - writer.writeTo(entity, entityClass, entityType, Serialisers.NO_ANNOTATION, mediaType, + writer.writeTo(entity, entityClass, entityType, context.getAllAnnotations(), mediaType, new QuarkusMultivaluedHashMap<>(), baos); wrote = true; break; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index d166fc9aebbed..a03c161fa1106 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -211,7 +211,7 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, // when a method is blocking, we also want all the request filters to run on the worker thread // because they can potentially set thread local variables - //we don't need to run this for Servlet and other runtimes that default to blocking + // we don't need to run this for Servlet and other runtimes that default to blocking Optional blockingHandlerIndex = Optional.empty(); if (!defaultBlocking) { if (method.isBlocking()) { diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/dependabot.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/dependabot.yml index 4b11d34c5ea02..5b063201e48db 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/dependabot.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/dependabot.yml @@ -1,13 +1,11 @@ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - - package-ecosystem: "maven" - directory: "/" + - package-ecosystem: "maven" # See documentation for possible values + directory: "/" # Location of package manifests schedule: interval: "daily" - ignore: - - dependency-name: "org.apache.maven.plugins:maven-compiler-plugin" diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute index 91fbf53ebbc5e..279d54e968b88 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-{java.version}:1.14 +FROM registry.access.redhat.com/ubi8/openjdk-{java.version}:1.15 ENV LANGUAGE='en_US:en' diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java index 8852072eaee86..5e14e7c86af70 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java @@ -55,7 +55,7 @@ public enum LayoutType { public static final String DEFAULT_QUARKIVERSE_PARENT_GROUP_ID = "io.quarkiverse"; public static final String DEFAULT_QUARKIVERSE_PARENT_ARTIFACT_ID = "quarkiverse-parent"; - public static final String DEFAULT_QUARKIVERSE_PARENT_VERSION = "12"; + public static final String DEFAULT_QUARKIVERSE_PARENT_VERSION = "13"; public static final String DEFAULT_QUARKIVERSE_NAMESPACE_ID = "quarkus-"; public static final String DEFAULT_QUARKIVERSE_GUIDE_URL = "https://quarkiverse.github.io/quarkiverse-docs/%s/dev/"; diff --git a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java index b0a167d704e74..5a0436c96b36a 100644 --- a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java +++ b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java @@ -297,13 +297,13 @@ private void checkDockerfilesWithMaven(Path projectDir) { assertThat(projectDir.resolve("src/main/docker/Dockerfile.jvm")).exists() .satisfies(checkContains("./mvnw package")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.jvm")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.14"))//TODO: make a teste to java17 + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.15"))//TODO: make a test for java17 .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) .satisfies(checkNotContains("ENTRYPOINT")); assertThat(projectDir.resolve("src/main/docker/Dockerfile.legacy-jar")).exists() .satisfies(checkContains("./mvnw package -Dquarkus.package.type=legacy-jar")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.legacy-jar")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.14")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-11:1.15")) .satisfies(checkContains("EXPOSE 8080")) .satisfies(checkContains("USER 185")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) diff --git a/integration-tests/hibernate-orm-envers/pom.xml b/integration-tests/hibernate-orm-envers/pom.xml index 2728418387329..ee23574489316 100644 --- a/integration-tests/hibernate-orm-envers/pom.xml +++ b/integration-tests/hibernate-orm-envers/pom.xml @@ -28,6 +28,10 @@ io.quarkus quarkus-jdbc-h2 + + io.quarkus + quarkus-jaxrs-client-reactive + @@ -99,6 +103,19 @@ + + io.quarkus + quarkus-jaxrs-client-reactive-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/CustomOutput.java b/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/CustomOutput.java new file mode 100644 index 0000000000000..503abf7959c71 --- /dev/null +++ b/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/CustomOutput.java @@ -0,0 +1,13 @@ +package io.quarkus.it.envers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CustomOutput { + + String value(); +} diff --git a/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/InputResource.java b/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/InputResource.java new file mode 100644 index 0000000000000..dcaf7d6677d2f --- /dev/null +++ b/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/InputResource.java @@ -0,0 +1,18 @@ +package io.quarkus.it.envers; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("input") +public class InputResource { + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.TEXT_PLAIN) + public String in(Message message) { + return message.getData(); + } +} diff --git a/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/Message.java b/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/Message.java new file mode 100644 index 0000000000000..666d59e77760b --- /dev/null +++ b/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/Message.java @@ -0,0 +1,21 @@ +package io.quarkus.it.envers; + +public class Message { + + private String data; + + public Message() { + } + + public Message(String data) { + this.data = data; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } +} diff --git a/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/MessageProvider.java b/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/MessageProvider.java new file mode 100644 index 0000000000000..3bbff668bba35 --- /dev/null +++ b/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/MessageProvider.java @@ -0,0 +1,54 @@ +package io.quarkus.it.envers; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; + +@Provider +@Consumes(MediaType.WILDCARD) +@Produces(MediaType.WILDCARD) +public class MessageProvider implements MessageBodyReader, MessageBodyWriter { + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return Message.class.isAssignableFrom(type); + } + + @Override + public Message readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { + return new Message("in"); + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return Message.class.isAssignableFrom(type); + } + + @Override + public void writeTo(Message event, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { + String data = "out"; + if (annotations != null) { + for (Annotation annotation : annotations) { + if (annotation.annotationType().equals(CustomOutput.class)) { + data = ((CustomOutput) annotation).value(); + break; + } + } + } + entityStream.write(String.format("{\"data\": \"%s\"}", data).getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/OutputResource.java b/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/OutputResource.java new file mode 100644 index 0000000000000..4376c1378611f --- /dev/null +++ b/integration-tests/hibernate-orm-envers/src/main/java/io/quarkus/it/envers/OutputResource.java @@ -0,0 +1,37 @@ +package io.quarkus.it.envers; + +import java.util.List; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.RestStreamElementType; + +import io.smallrye.mutiny.Multi; + +@Path("output") +public class OutputResource { + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Message out() { + return new Message("test"); + } + + @GET + @RestStreamElementType(MediaType.APPLICATION_JSON) + @CustomOutput("dummy") + @Path("annotation") + public Multi sseOut() { + return Multi.createFrom().iterable(List.of(new Message("test"), new Message("test"))); + } + + @GET + @RestStreamElementType(MediaType.APPLICATION_JSON) + @Path("no-annotation") + public Multi sseOut2() { + return Multi.createFrom().iterable(List.of(new Message("test"), new Message("test"))); + } +} diff --git a/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/InputResourceIT.java b/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/InputResourceIT.java new file mode 100644 index 0000000000000..20207a0df11b3 --- /dev/null +++ b/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/InputResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.envers; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class InputResourceIT extends InputResourceTest { +} diff --git a/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/InputResourceTest.java b/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/InputResourceTest.java new file mode 100644 index 0000000000000..bf543e476363d --- /dev/null +++ b/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/InputResourceTest.java @@ -0,0 +1,22 @@ +package io.quarkus.it.envers; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; + +@QuarkusTest +class InputResourceTest { + + @Test + void test() { + given().contentType(ContentType.JSON).accept(ContentType.TEXT) + .when().post("/jpa-envers-test/input") + .then() + .statusCode(200) + .body(equalTo("in")); + } +} diff --git a/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/OutputResourceIT.java b/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/OutputResourceIT.java new file mode 100644 index 0000000000000..cf03bae458b4a --- /dev/null +++ b/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/OutputResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.envers; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class OutputResourceIT extends OutputResourceTest { +} diff --git a/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/OutputResourceTest.java b/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/OutputResourceTest.java new file mode 100644 index 0000000000000..74b6aa4ffa9bb --- /dev/null +++ b/integration-tests/hibernate-orm-envers/src/test/java/io/quarkus/it/envers/OutputResourceTest.java @@ -0,0 +1,75 @@ +package io.quarkus.it.envers; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.sse.SseEventSource; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; + +@QuarkusTest +class OutputResourceTest { + + private static final String RESOURCE_PATH = "/jpa-envers-test/output"; + @TestHTTPEndpoint(OutputResource.class) + @TestHTTPResource + URL url; + + @Test + void test() { + given().accept(ContentType.JSON) + .when() + .get(RESOURCE_PATH) + .then() + .statusCode(200) + .body("data", equalTo("out")); + } + + @Test + public void testSseWithAnnotation() throws InterruptedException, URISyntaxException, MalformedURLException { + doTestSee("annotation", "dummy"); + } + + @Test + public void testSseWithoutAnnotation() throws InterruptedException, URISyntaxException, MalformedURLException { + doTestSee("no-annotation", "out"); + } + + private void doTestSee(String path, String expectedDataValue) + throws URISyntaxException, InterruptedException, MalformedURLException { + Client client = ClientBuilder.newBuilder().build(); + WebTarget target = client.target(new URL(ConfigProvider.getConfig().getValue("test.url", String.class)).toURI()) + .path(RESOURCE_PATH).path(path); + try (SseEventSource sse = SseEventSource.target(target).build()) { + CountDownLatch latch = new CountDownLatch(1); + List errors = new CopyOnWriteArrayList<>(); + List results = new CopyOnWriteArrayList<>(); + sse.register(event -> results.add(event.readData()), errors::add, latch::countDown); + sse.open(); + Assertions.assertTrue(latch.await(20, TimeUnit.SECONDS)); + + String json = String.format("{\"data\": \"%s\"}", expectedDataValue); + Assertions.assertEquals(Arrays.asList(json, json), results); + Assertions.assertEquals(0, errors.size()); + } + } +} diff --git a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java index 241f7bf51e6e5..57e872960e8a1 100644 --- a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java +++ b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java @@ -6,21 +6,17 @@ import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.test.TestReactiveTransaction; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.vertx.RunOnVertxContext; import io.quarkus.test.vertx.UniAsserter; @QuarkusTest +@TestReactiveTransaction public class TestReactiveTransactionTest { - @RunOnVertxContext - @TestReactiveTransaction @Test public void testTestTransaction(UniAsserter asserter) { asserter.assertNotNull(() -> Panache.currentTransaction()); } - @RunOnVertxContext - @TestReactiveTransaction @BeforeEach public void beforeEach(UniAsserter asserter) { asserter.assertNotNull(() -> Panache.currentTransaction()); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacFullTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacFullTest.java index 41bdd6c3e594f..83be5e5411d86 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacFullTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRbacFullTest.java @@ -53,6 +53,12 @@ public void assertGeneratedResources() throws IOException { assertTrue(lastIndexOfRoleRefKind < firstIndexOfRoleBinding, "RoleBinding resource is created before " + "the Role/ClusterRole/ServiceAccount resource!"); + // ensure service account resource is generated before the Deployment resource: + int lastIndexOfServiceAccount = lastIndexOfKind(kubernetesFileContent, "ServiceAccount"); + int firstIndexOfDeployment = kubernetesFileContent.indexOf("kind: Deployment"); + assertTrue(lastIndexOfServiceAccount < firstIndexOfDeployment, "ServiceAccount resource is created after " + + "the Deployment resource!"); + List kubernetesList = DeserializationUtil.deserializeAsList(kubernetesFile); Deployment deployment = getDeploymentByName(kubernetesList, APP_NAME); diff --git a/integration-tests/management-interface-auth/pom.xml b/integration-tests/management-interface-auth/pom.xml index 7bdf390f8b792..2c2a74773dc53 100644 --- a/integration-tests/management-interface-auth/pom.xml +++ b/integration-tests/management-interface-auth/pom.xml @@ -30,6 +30,10 @@ io.quarkus quarkus-elytron-security-properties-file + + io.quarkus + quarkus-smallrye-jwt + io.quarkus quarkus-junit5 @@ -99,6 +103,19 @@ + + io.quarkus + quarkus-smallrye-jwt-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests/management-interface-auth/src/main/java/io/quarkus/it/management/GreetingResource.java b/integration-tests/management-interface-auth/src/main/java/io/quarkus/it/management/GreetingResource.java index 1557a8d3bd791..6526c8505b21a 100644 --- a/integration-tests/management-interface-auth/src/main/java/io/quarkus/it/management/GreetingResource.java +++ b/integration-tests/management-interface-auth/src/main/java/io/quarkus/it/management/GreetingResource.java @@ -17,4 +17,16 @@ public String hello() { public String goodbye() { return "goodbye"; } + + @GET + @Path("/goodmorning") + public String goodmorning() { + return "goodmorning"; + } + + @GET + @Path("/goodevening") + public String goodevening() { + return "goodevening"; + } } diff --git a/integration-tests/management-interface-auth/src/main/resources/application.properties b/integration-tests/management-interface-auth/src/main/resources/application.properties index 51b3e507c06c3..6844b7e6ce612 100644 --- a/integration-tests/management-interface-auth/src/main/resources/application.properties +++ b/integration-tests/management-interface-auth/src/main/resources/application.properties @@ -1,15 +1,28 @@ quarkus.management.enabled=true +# Management router authentication: +# /q/health/* - basic authentication only, `management` role is required +# /q/metrics/* - basic and jwt authentications are allowed, any role is allowed quarkus.management.auth.basic=true quarkus.management.auth.policy.role-policy.roles-allowed=management quarkus.management.auth.permission.health.paths=/q/health/* quarkus.management.auth.permission.health.policy=role-policy +quarkus.management.auth.permission.health.auth-mechanism=basic + quarkus.management.auth.permission.metrics.paths=/q/metrics/* quarkus.management.auth.permission.metrics.policy=authenticated +# Main router authentication: +# /service/hello/* - public resource +# /service/goodbye/* - basic authentication only, `greeting` role is allowed +# /service/goodmorning/* - JWT bearer authentication only, `admin` role is allowed +# /service/goodevening/* - JWT bearer authentication only, `user` role is allowed + quarkus.http.auth.basic=true quarkus.http.auth.policy.role-policy.roles-allowed=greeting + quarkus.http.auth.permission.main.paths=/service/goodbye quarkus.http.auth.permission.main.policy=role-policy +quarkus.http.auth.permission.main.auth-mechanism=basic quarkus.security.users.embedded.enabled=true quarkus.security.users.embedded.plain-text=true @@ -18,3 +31,18 @@ quarkus.security.users.embedded.roles.alice=management quarkus.security.users.embedded.users.bob=bob quarkus.security.users.embedded.roles.bob=greeting + +quarkus.http.auth.policy.role-policy-jwt-admin.roles-allowed=admin +quarkus.http.auth.permission.main-jwt-admin.paths=/service/goodmorning +quarkus.http.auth.permission.main-jwt-admin.policy=role-policy-jwt-admin +quarkus.http.auth.permission.main-jwt-admin.auth-mechanism=bearer + +quarkus.http.auth.policy.role-policy-jwt-user.roles-allowed=user +quarkus.http.auth.permission.main-jwt-user.paths=/service/goodevening +quarkus.http.auth.permission.main-jwt-user.policy=role-policy-jwt-user +quarkus.http.auth.permission.main-jwt-user.auth-mechanism=bearer + +mp.jwt.verify.publickey.location=/publicKey.pem +mp.jwt.verify.issuer=https://server.example.com +smallrye.jwt.sign.key.location=/privateKey.pem +smallrye.jwt.new-token.issuer=https://server.example.com \ No newline at end of file diff --git a/integration-tests/management-interface-auth/src/main/resources/privateKey.pem b/integration-tests/management-interface-auth/src/main/resources/privateKey.pem new file mode 100644 index 0000000000000..27543a434a1eb --- /dev/null +++ b/integration-tests/management-interface-auth/src/main/resources/privateKey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa +PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H +OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN +qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh +nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM +uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6 +oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv +6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY +URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6 +96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB +Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3 +zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF +KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP +iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B +m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS +34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG +5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2 +tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL +WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y +b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09 +nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB +MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d +Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe +Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt +FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8 +f3cg+fr8aou7pr9SHhJlZCU= +-----END PRIVATE KEY----- diff --git a/integration-tests/management-interface-auth/src/main/resources/publicKey.pem b/integration-tests/management-interface-auth/src/main/resources/publicKey.pem new file mode 100644 index 0000000000000..6dc936fca3485 --- /dev/null +++ b/integration-tests/management-interface-auth/src/main/resources/publicKey.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq +Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR +TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e +UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9 +AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn +sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x +nQIDAQAB +-----END PUBLIC KEY----- diff --git a/integration-tests/management-interface-auth/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java b/integration-tests/management-interface-auth/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java index 48f9a68972add..ba49cf5b1f813 100644 --- a/integration-tests/management-interface-auth/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java +++ b/integration-tests/management-interface-auth/src/test/java/io/quarkus/it/management/ManagementInterfaceTestCase.java @@ -1,10 +1,13 @@ package io.quarkus.it.management; +import java.util.Set; + import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; +import io.smallrye.jwt.build.Jwt; @QuarkusTest public class ManagementInterfaceTestCase { @@ -29,6 +32,11 @@ void verifyThatHealthChecksAreExposedOnManagementInterface() { .then().statusCode(200) .body(Matchers.containsString("UP")); + RestAssured.given().auth().oauth2(getAdminToken()).get(getPrefix() + "/q/health") + .then().statusCode(401); + RestAssured.given().auth().oauth2(getUserToken()).get(getPrefix() + "/q/health") + .then().statusCode(401); + RestAssured.get("/q/health") .then().statusCode(404); } @@ -44,12 +52,19 @@ void verifyThatMetricsAreExposedOnManagementInterface() { RestAssured.given().auth().basic("john", "john").get(getPrefix() + "/q/metrics") .then().statusCode(401); + RestAssured.given().auth().oauth2(getAdminToken()).get(getPrefix() + "/q/metrics") + .then().statusCode(200); + RestAssured.given().auth().oauth2(getUserToken()).get(getPrefix() + "/q/metrics") + .then().statusCode(200); + RestAssured.given().auth().oauth2("wrongtoken").get(getPrefix() + "/q/metrics") + .then().statusCode(401); + RestAssured.get("/q/metrics") .then().statusCode(404); } @Test - void verifyMainEndpoint() { + void verifyMainEndpointBasicAuth() { RestAssured.get("/service/hello").then().statusCode(200) .body(Matchers.equalTo("hello")); @@ -62,5 +77,55 @@ void verifyMainEndpoint() { RestAssured.given().auth().basic("bob", "bob").get("/service/goodbye") .then().statusCode(200) .body(Matchers.equalTo("goodbye")); + + RestAssured.given().auth().oauth2(getAdminToken()).get("/service/goodbye") + .then().statusCode(401); + RestAssured.given().auth().oauth2(getUserToken()).get("/service/goodbye") + .then().statusCode(401); + + } + + @Test + void verifyMainEndpointJwtAuth() { + RestAssured.get("/service/hello").then().statusCode(200) + .body(Matchers.equalTo("hello")); + + RestAssured.given().auth().preemptive().basic("john", "john").get("/service/goodmorning") + .then().statusCode(401); + RestAssured.given().auth().preemptive().basic("john", "john").get("/service/goodevening") + .then().statusCode(401); + + RestAssured.given().auth().preemptive().basic("alice", "alice").get("/service/goodmorning") + .then().statusCode(401); + RestAssured.given().auth().preemptive().basic("alice", "alice").get("/service/goodevening") + .then().statusCode(401); + + RestAssured.given().auth().basic("bob", "bob").get("/service/goodmorning") + .then().statusCode(401); + RestAssured.given().auth().basic("bob", "bob").get("/service/goodevening") + .then().statusCode(401); + + RestAssured.given().auth().oauth2(getAdminToken()).get("/service/goodmorning") + .then().statusCode(200) + .body(Matchers.equalTo("goodmorning")); + RestAssured.given().auth().oauth2(getUserToken()).get("/service/goodmorning") + .then().statusCode(403); + + RestAssured.given().auth().oauth2(getAdminToken()).get("/service/goodevening") + .then().statusCode(200) + .body(Matchers.equalTo("goodevening")); + RestAssured.given().auth().oauth2(getUserToken()).get("/service/goodevening") + .then().statusCode(200) + .body(Matchers.equalTo("goodevening")); + } + + private String getAdminToken() { + return Jwt.upn("alice").groups(Set.of("admin", "user")).sign(); + } + + private String getUserToken() { + return Jwt.subject("bob").groups("user").sign(); + } + } diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/RemoteDevMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/RemoteDevMojoIT.java index 8ecc9ff41e6e0..9732478d18c47 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/RemoteDevMojoIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/RemoteDevMojoIT.java @@ -57,6 +57,7 @@ public void testThatTheApplicationIsReloadedOnJavaChange() //also verify that the dev ui console is disabled DevModeTestUtils.getHttpResponse("/q/dev-v1", 404, 10, TimeUnit.SECONDS); + DevModeTestUtils.getHttpResponse("/q/dev-ui", 404, 10, TimeUnit.SECONDS); } @Test diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml index 79ef456dd18ba..f2bcd7fc4f610 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml @@ -5,7 +5,7 @@ io.quarkiverse quarkiverse-parent - 12 + 13 io.quarkiverse.my-quarkiverse-ext quarkus-my-quarkiverse-ext-parent diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java index 5c8ce5297d4df..80a7540178d38 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java @@ -137,6 +137,8 @@ public OidcTenantConfig get() { config.setTokenPath(tokenUri); String jwksUri = uri.replace("/tenant-refresh/tenant-web-app-refresh/api/user", "/oidc/jwks"); config.setJwksPath(jwksUri); + String userInfoPath = uri.replace("/tenant-refresh/tenant-web-app-refresh/api/user", "/oidc/userinfo"); + config.setUserInfoPath(userInfoPath); config.getToken().setIssuer("any"); config.tokenStateManager.setSplitTokens(true); config.getAuthentication().setSessionAgeExtension(Duration.ofMinutes(1)); diff --git a/integration-tests/oidc-tenancy/src/main/resources/application.properties b/integration-tests/oidc-tenancy/src/main/resources/application.properties index 0074691749591..af8297b86a941 100644 --- a/integration-tests/oidc-tenancy/src/main/resources/application.properties +++ b/integration-tests/oidc-tenancy/src/main/resources/application.properties @@ -49,7 +49,6 @@ quarkus.oidc.tenant-web-app.auth-server-url=${keycloak.url}/realms/quarkus-webap quarkus.oidc.tenant-web-app.client-id=quarkus-app-webapp quarkus.oidc.tenant-web-app.credentials.secret=secret quarkus.oidc.tenant-web-app.application-type=web-app -quarkus.oidc.tenant-web-app.authentication.user-info-required=true quarkus.oidc.tenant-web-app.roles.source=userinfo quarkus.oidc.tenant-web-app.allow-user-info-cache=false diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index cd675540342b3..dfc8b39209b9b 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -56,7 +56,6 @@ quarkus.oidc.code-flow-user-info-only.authorization-path=/ quarkus.oidc.code-flow-user-info-only.token-path=access_token quarkus.oidc.code-flow-user-info-only.user-info-path=protocol/openid-connect/userinfo quarkus.oidc.code-flow-user-info-only.authentication.id-token-required=false -quarkus.oidc.code-flow-user-info-only.authentication.user-info-required=true quarkus.oidc.code-flow-user-info-only.code-grant.extra-params.extra-param=extra-param-value quarkus.oidc.code-flow-user-info-only.code-grant.headers.X-Custom=XCustomHeaderValue quarkus.oidc.code-flow-user-info-only.client-id=quarkus-web-app diff --git a/integration-tests/opentelemetry-jdbc-instrumentation/src/main/resources/application.properties b/integration-tests/opentelemetry-jdbc-instrumentation/src/main/resources/application.properties index e26c2fced8a45..93429306dc891 100644 --- a/integration-tests/opentelemetry-jdbc-instrumentation/src/main/resources/application.properties +++ b/integration-tests/opentelemetry-jdbc-instrumentation/src/main/resources/application.properties @@ -40,3 +40,7 @@ quarkus.hibernate-orm.db2.database.generation=none quarkus.datasource.db2.db-kind=db2 quarkus.datasource.db2.jdbc.max-size=1 quarkus.datasource.db2.jdbc.telemetry=true + +# speed up build +quarkus.otel.bsp.schedule.delay=100 +quarkus.otel.bsp.export.timeout=5s diff --git a/integration-tests/opentelemetry-jdbc-instrumentation/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryJdbcInstrumentationTest.java b/integration-tests/opentelemetry-jdbc-instrumentation/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryJdbcInstrumentationTest.java index 41a27b3731d8b..8dde3f5b0ac02 100644 --- a/integration-tests/opentelemetry-jdbc-instrumentation/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryJdbcInstrumentationTest.java +++ b/integration-tests/opentelemetry-jdbc-instrumentation/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryJdbcInstrumentationTest.java @@ -5,6 +5,7 @@ import static java.net.HttpURLConnection.HTTP_OK; import static java.util.concurrent.TimeUnit.SECONDS; import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Duration; @@ -47,22 +48,24 @@ protected void testQueryTraced(String dbKind, String expectedTable) { .statusCode(200) .body("message", Matchers.equalTo("Hit message.")); - Awaitility.await().atMost(Duration.ofSeconds(55)).until(() -> !getSpans().isEmpty()); + Awaitility.await().atMost(Duration.ofSeconds(55)).untilAsserted(() -> { + assertFalse(getSpans().isEmpty()); - // Assert insert has been traced - boolean hitInserted = false; - for (Map spanData : getSpans()) { - if (spanData.get("attributes") instanceof Map) { - final Map attributes = (Map) spanData.get("attributes"); - var dbOperation = attributes.get("db.operation"); - var dbTable = attributes.get("db.sql.table"); - if ("INSERT".equals(dbOperation) && expectedTable.equals(dbTable)) { - hitInserted = true; - break; + // Assert insert has been traced + boolean hitInserted = false; + for (Map spanData : getSpans()) { + if (spanData.get("attributes") instanceof Map) { + final Map attributes = (Map) spanData.get("attributes"); + var dbOperation = attributes.get("db.operation"); + var dbTable = attributes.get("db.sql.table"); + if ("INSERT".equals(dbOperation) && expectedTable.equals(dbTable)) { + hitInserted = true; + break; + } } } - } - assertTrue(hitInserted, "JDBC insert statement was not traced."); + assertTrue(hitInserted, "JDBC insert statement was not traced."); + }); } } diff --git a/integration-tests/opentelemetry-jdbc-instrumentation/src/test/java/io/quarkus/it/opentelemetry/PostgresOpenTelemetryJdbcInstrumentationTest.java b/integration-tests/opentelemetry-jdbc-instrumentation/src/test/java/io/quarkus/it/opentelemetry/PostgresOpenTelemetryJdbcInstrumentationTest.java index 6deaade8b59d2..cde8956e413eb 100644 --- a/integration-tests/opentelemetry-jdbc-instrumentation/src/test/java/io/quarkus/it/opentelemetry/PostgresOpenTelemetryJdbcInstrumentationTest.java +++ b/integration-tests/opentelemetry-jdbc-instrumentation/src/test/java/io/quarkus/it/opentelemetry/PostgresOpenTelemetryJdbcInstrumentationTest.java @@ -1,6 +1,5 @@ package io.quarkus.it.opentelemetry; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import io.quarkus.test.common.QuarkusTestResource; @@ -8,7 +7,6 @@ @QuarkusTest @QuarkusTestResource(value = PostgreSqlLifecycleManager.class, restrictToAnnotatedClass = true) -@Disabled("flaky test") public class PostgresOpenTelemetryJdbcInstrumentationTest extends OpenTelemetryJdbcInstrumentationTest { @Test diff --git a/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/Http2TestCase.java b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/Http2TestCase.java index d140349416e12..58ccc474cd1d3 100644 --- a/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/Http2TestCase.java +++ b/integration-tests/vertx-http/src/test/java/io/quarkus/it/vertx/Http2TestCase.java @@ -49,7 +49,7 @@ public void testHttp2EnabledSslWithNotSelectedClientCert() throws ExecutionExcep ExecutionException exc = Assertions.assertThrows(ExecutionException.class, () -> runHttp2EnabledSsl("client-keystore-2.jks")); - Assertions.assertEquals("SSLHandshakeException: Received fatal alert: bad_certificate", + Assertions.assertEquals("VertxException: Connection was closed", ExceptionUtils.getRootCauseMessage(exc)); } diff --git a/test-framework/vertx/src/main/java/io/quarkus/test/vertx/RunOnVertxContextTestMethodInvoker.java b/test-framework/vertx/src/main/java/io/quarkus/test/vertx/RunOnVertxContextTestMethodInvoker.java index a348c0a83c430..39214de3f94b3 100644 --- a/test-framework/vertx/src/main/java/io/quarkus/test/vertx/RunOnVertxContextTestMethodInvoker.java +++ b/test-framework/vertx/src/main/java/io/quarkus/test/vertx/RunOnVertxContextTestMethodInvoker.java @@ -99,7 +99,8 @@ private boolean shouldContextBeDuplicated(Class c, Method m) { } if (runOnVertxContext == null) { // Use duplicated context if @TestReactiveTransaction is present - return m.isAnnotationPresent(TestReactiveTransaction.class); + return m.isAnnotationPresent(TestReactiveTransaction.class) + || m.getDeclaringClass().isAnnotationPresent(TestReactiveTransaction.class); } else { return runOnVertxContext.duplicateContext(); }