diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 8cdc7d981bbee..e68b8a6b28360 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -42,7 +42,7 @@ 1.2 1.0 1.8.0 - 2.6.1 + 2.7.0 3.1.2 3.0.4 2.1.16 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java index 112b5fe661a7f..39a650eebf009 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java @@ -92,7 +92,7 @@ public static boolean trigger(ClassLoader deploymentClassLoader, final PropertiesConfigSource pcs = new PropertiesConfigSource(properties, "Build system"); - final SmallRyeConfig config = ConfigUtils.configBuilder(false, launchMode) + final SmallRyeConfig config = ConfigUtils.configBuilder(false, false, launchMode) .withProfile(launchMode.getDefaultProfile()) .withSources(pcs) .build(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java index c0f89db2efa23..f1dd739fa2ff9 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildContainerRunner.java @@ -148,7 +148,8 @@ private static NativeConfig.ContainerRuntime detectContainerRuntime() { } else if (podmanAvailable) { return NativeConfig.ContainerRuntime.PODMAN; } else { - throw new IllegalStateException("No container runtime was found to run the native image builder"); + throw new IllegalStateException("No container runtime was found to run the native image builder. " + + "Make sure you have Docker or Podman installed in your environment."); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index 4cb38ae1bc341..086c43b108877 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -212,7 +212,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa resultingExecutableName, outputDir, nativeConfig.debug.enabled, processInheritIODisabled.isPresent()); if (buildNativeResult.getExitCode() != 0) { - throw imageGenerationFailed(buildNativeResult.getExitCode(), nativeImageArgs); + throw imageGenerationFailed(buildNativeResult.getExitCode(), nativeConfig.isContainerBuild()); } IoUtils.copy(generatedExecutablePath, finalExecutablePath); Files.delete(generatedExecutablePath); @@ -357,9 +357,9 @@ private static void copySourcesToSourceCache(OutputTargetBuildItem outputTargetB } } - private RuntimeException imageGenerationFailed(int exitValue, List command) { + private RuntimeException imageGenerationFailed(int exitValue, boolean isContainerBuild) { if (exitValue == OOM_ERROR_VALUE) { - if (command.contains("docker") && !SystemUtils.IS_OS_LINUX) { + if (isContainerBuild && !SystemUtils.IS_OS_LINUX) { return new RuntimeException("Image generation failed. Exit code was " + exitValue + " which indicates an out of memory error. The most likely cause is Docker not being given enough memory. Also consider increasing the Xmx value for native image generation by setting the \"" + QUARKUS_XMX_PROPERTY + "\" property"); @@ -603,6 +603,9 @@ public NativeImageInvokerInfo build() { */ nativeImageArgs.add("-J--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED"); + //address https://github.com/quarkusio/quarkus-quickstarts/issues/993 + nativeImageArgs.add("-J--add-opens=java.base/java.text=ALL-UNNAMED"); + handleAdditionalProperties(nativeImageArgs); nativeImageArgs.add( diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java index c70fd66ca4966..46591c93c5f0e 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java @@ -165,6 +165,7 @@ public void execute(Task test) { }); tasks.named(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME, JavaCompile.class, compileTestJava -> { + compileTestJava.dependsOn(quarkusGenerateCode); compileTestJava.dependsOn(quarkusGenerateCodeTests); if (project.getGradle().getStartParameter().getTaskNames().contains(QUARKUS_DEV_TASK_NAME)) { compileTestJava.getOptions().setFailOnError(false); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/dependency/DependencyUtils.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/dependency/DependencyUtils.java index 8ff3715d08351..08d6e2918cd34 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/dependency/DependencyUtils.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/dependency/DependencyUtils.java @@ -119,11 +119,13 @@ public static ExtensionDependency getExtensionInfoOrNull(Project project, Resolv ((ProjectComponentIdentifier) artifact.getId().getComponentIdentifier()).getProjectPath()); final JavaPluginConvention javaExtension = projectDep == null ? null : projectDep.getConvention().findPlugin(JavaPluginConvention.class); - SourceSet mainSourceSet = javaExtension.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME); - File resourcesDir = mainSourceSet.getOutput().getResourcesDir(); - Path descriptorPath = resourcesDir.toPath().resolve(BootstrapConstants.DESCRIPTOR_PATH); - if (Files.exists(descriptorPath)) { - return loadExtensionInfo(project, descriptorPath, artifactId, projectDep); + if (javaExtension != null) { + SourceSet mainSourceSet = javaExtension.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME); + File resourcesDir = mainSourceSet.getOutput().getResourcesDir(); + Path descriptorPath = resourcesDir.toPath().resolve(BootstrapConstants.DESCRIPTOR_PATH); + if (Files.exists(descriptorPath)) { + return loadExtensionInfo(project, descriptorPath, artifactId, projectDep); + } } } diff --git a/docs/src/main/asciidoc/deploying-to-openshift.adoc b/docs/src/main/asciidoc/deploying-to-openshift.adoc index f5b44844cf70e..5b3d76bbfa331 100644 --- a/docs/src/main/asciidoc/deploying-to-openshift.adoc +++ b/docs/src/main/asciidoc/deploying-to-openshift.adoc @@ -93,12 +93,14 @@ To trigger a build and deployment in a single step: ./mvnw clean package -Dquarkus.kubernetes.deploy=true ---- +TIP: If you want to test your application immediately then set the `quarkus.openshift.route.expose` config property to `true` to <>, e.g. add `-Dquarkus.openshift.route.expose=true` to the command above. + This command will build your application locally, then trigger a container image build and finally apply the generated OpenShift resources automatically. The generated resources use OpenShift's `DeploymentConfig` that is configured to automatically trigger a redeployment when a change in the `ImageStream` is noticed. In other words, any container image build after the initial deployment will automatically trigger redeployment, without the need to delete, update or re-apply the generated resources. -To confirm the above command has created an image stream, a service resource and has deployed the application (has a pod running), apply these commands: - +You can use the OpenShift web console to verify that the above command has created an image stream, a service resource and has deployed the application. +Alternatively, you can run the following OpenShift CLI commands: [source,bash,subs=attributes+] ---- oc get is <1> @@ -109,9 +111,10 @@ oc get svc <3> <2> Get the list of pods. <3> Get the list of Kubernetes services. -By default, the service is not exposed to the outside world. -To expose the created service to a route and test it: +Note that the service is not exposed to the outside world by default. +So unless you've used the `quarkus.openshift.route.expose` config property to expose the created service automatically you'll need to expose the service manually. +.Expose The Service - OpenShift CLI Example [source,bash,subs=attributes+] ---- oc expose svc/greeting <1> @@ -195,6 +198,7 @@ All available customization options are available in the link:deploying-to-kuber Some examples are provided in the sections below: +[[exposing_routes]] === Exposing Routes To expose a `Route` for the Quarkus application: diff --git a/docs/src/main/asciidoc/flyway.adoc b/docs/src/main/asciidoc/flyway.adoc index f9017d6a4f9de..15957d2b656fe 100644 --- a/docs/src/main/asciidoc/flyway.adoc +++ b/docs/src/main/asciidoc/flyway.adoc @@ -25,7 +25,7 @@ In your `pom.xml`, add the following dependencies: * the Flyway extension * your JDBC driver extension (`quarkus-jdbc-postgresql`, `quarkus-jdbc-h2`, `quarkus-jdbc-mariadb`, ...) - +* the SQL Server support is now in a separate dependency, SQL Server users need to add the `flyway-sqlserver` dependency from now on. [source,xml] ---- @@ -34,6 +34,12 @@ In your `pom.xml`, add the following dependencies: io.quarkus quarkus-flyway + + + + org.flywaydb + flyway-sqlserver + diff --git a/docs/src/main/asciidoc/ide-tooling.adoc b/docs/src/main/asciidoc/ide-tooling.adoc index a41e587d9c996..1cfd441f15c8b 100644 --- a/docs/src/main/asciidoc/ide-tooling.adoc +++ b/docs/src/main/asciidoc/ide-tooling.adoc @@ -69,9 +69,9 @@ https://download.jboss.org/jbosstools/intellij/snapshots/intellij-quarkus/[Devel | https://che.openshift.io/f?url=https://raw.githubusercontent.com/redhat-developer/devfile/master/getting-started/quarkus/devfile.yaml[Start Che Workspace] |Source -|https://github.com/redhat-developer/vscode-quarkus[Github] -|https://github.com/jbosstools/jbosstools-quarkus[Github] -|https://github.com/redhat-developer/intellij-quarkus[Github] +|https://github.com/redhat-developer/vscode-quarkus[GitHub] +|https://github.com/jbosstools/jbosstools-quarkus[GitHub] +|https://github.com/redhat-developer/intellij-quarkus[GitHub] |Closed-Source | diff --git a/docs/src/main/asciidoc/reactive-routes.adoc b/docs/src/main/asciidoc/reactive-routes.adoc index 71b25befeffc3..7541983e3375a 100644 --- a/docs/src/main/asciidoc/reactive-routes.adoc +++ b/docs/src/main/asciidoc/reactive-routes.adoc @@ -12,7 +12,7 @@ This approach became very popular in the JavaScript world, with frameworks like Quarkus also offers the possibility to use reactive routes. You can implement REST API with routes only or combine them with JAX-RS resources and servlets. -The code presented in this guide is available in this {quickstarts-base-url}[Github repository] under the {quickstarts-tree-url}/reactive-routes-quickstart[`reactive-routes-quickstart` directory] +The code presented in this guide is available in this {quickstarts-base-url}[GitHub repository] under the {quickstarts-tree-url}/reactive-routes-quickstart[`reactive-routes-quickstart` directory] NOTE: Reactive Routes were initially introduced to provide a reactive execution model for HTTP APIs on top of the xref:quarkus-reactive-architecture.adoc[Quarkus Reactive Architecture]. With the introduction of link:resteasy-reactive[RESTEasy Reactive], you can now implement reactive HTTP APIs and still use JAX-RS annotations. diff --git a/docs/src/main/asciidoc/redis-dev-services.adoc b/docs/src/main/asciidoc/redis-dev-services.adoc index 494ab3a88163b..ff0bd7ae37c12 100644 --- a/docs/src/main/asciidoc/redis-dev-services.adoc +++ b/docs/src/main/asciidoc/redis-dev-services.adoc @@ -13,6 +13,7 @@ What that means practically, is that if you have docker running and have not con Quarkus will automatically start a Redis container when running tests or dev-mode, and automatically configure the connection. Available properties to customize the Redis DevService. + include::{generated-dir}/config/quarkus-redis-client-config-group-dev-services-config.adoc[opts=optional, leveloffset=+1] When running the production version of the application, the Redis connection need to be configured as normal, diff --git a/docs/src/main/asciidoc/rest-json.adoc b/docs/src/main/asciidoc/rest-json.adoc index c1ca83483c5a1..f0adb9aa612ba 100644 --- a/docs/src/main/asciidoc/rest-json.adoc +++ b/docs/src/main/asciidoc/rest-json.adoc @@ -68,7 +68,7 @@ and in particular adds the following dependency: To improve user experience, Quarkus registers the three Jackson https://github.com/FasterXML/jackson-modules-java8[Java 8 modules] so you don't need to do it manually. ==== -Quarkus also supports http://json-b.net/[JSON-B] so, if you prefer JSON-B over Jackson, you can create a project relying on the RESTEasy JSON-B extension instead: +Quarkus also supports https://eclipse-ee4j.github.io/jsonb-api/[JSON-B] so, if you prefer JSON-B over Jackson, you can create a project relying on the RESTEasy JSON-B extension instead: [source,bash,subs=attributes+] ---- @@ -81,7 +81,7 @@ mvn io.quarkus.platform:quarkus-maven-plugin:{quarkus-version}:create \ cd rest-json-quickstart ---- -This command generates a Maven structure importing the RESTEasy/JAX-RS and http://json-b.net/[JSON-B] extensions, +This command generates a Maven structure importing the RESTEasy/JAX-RS and https://eclipse-ee4j.github.io/jsonb-api/[JSON-B] extensions, and in particular adds the following dependency: [source,xml] @@ -165,7 +165,7 @@ public class FruitResource { The implementation is pretty straightforward and you just need to define your endpoints using the JAX-RS annotations. -The `Fruit` objects will be automatically serialized/deserialized by http://json-b.net/[JSON-B] or https://github.com/FasterXML/jackson[Jackson], +The `Fruit` objects will be automatically serialized/deserialized by https://eclipse-ee4j.github.io/jsonb-api/[JSON-B] or https://github.com/FasterXML/jackson[Jackson], depending on the extension you chose when initializing the project. [NOTE] diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 330b7ef1c83c8..f6ef97c0023a1 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -913,7 +913,7 @@ Instead of importing `io.quarkus:quarkus-resteasy-reactive`, you can import eith |https://github.com/FasterXML/jackson[Jackson support] |`io.quarkus:quarkus-resteasy-reactive-jsonb` -|http://json-b.net/[JSONB support] +|https://eclipse-ee4j.github.io/jsonb-api/[JSONB support] |=== diff --git a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc index 33e5fc38cb094..68709f33a7637 100644 --- a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc @@ -350,7 +350,7 @@ Additionally a custom `SecurityIdentityAugmentor` can also be used to add the ro [[token-verification-introspection]] === Token Verification And Introspection -Please see link:security-openid-connect#token-verification-introspection for details about how the tokens are verified and introspected. +Please see link:security-openid-connect#token-verification-introspection[Token Verification And Introspection] for details about how the tokens are verified and introspected. Note that in case of `web-app` applications only `IdToken` is verified by default since the access token is not used by default to access the current Quarkus `web-app` endpoint and instead meant to be propagated to the services expecting this access token, for example, to the OpenId Connect Provider's UserInfo endpoint, etc. However if you expect the access token to contain the roles required to access the current Quarkus endpoint (`quarkus.oidc.roles.source=accesstoken`) then it will also be verified. @@ -359,7 +359,7 @@ Note that in case of `web-app` applications only `IdToken` is verified by defaul Code flow access tokens are not introspected unless they are expected to be the source of roles but will be used to get `UserInfo`. So there will be one or two remote calls with the code flow access token, if the token introspection and/or `UserInfo` are required. -Please see link:security-openid-connect#token-introspection-userinfo-cache for more information about using a default token cache or registering a custom cache implementation. +Please see link:security-openid-connect#token-introspection-userinfo-cache[Token Introspection and UserInfo cache] for more information about using a default token cache or registering a custom cache implementation. [[jwt-claim-verification]] === JSON Web Token Claim Verification diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc index 5e5f082c7c99e..94eb69408d818 100644 --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -3116,3 +3116,23 @@ In order to make it easy for extension authors to test their extensions daily ag the notion of Ecosystem CI. The Ecosystem CI link:https://github.com/quarkusio/quarkus-ecosystem-ci/blob/main/README.adoc[README] has all the details on how to set up a GitHub Actions job to take advantage of this capability, while this link:https://www.youtube.com/watch?v=VpbRA1n0hHQ[video] provides an overview of what the process looks like. + +== Publish your extension in registry.quarkus.io + +Before publishing your extension to the link:https://quarkus.io/guides/tooling[Quarkus tooling], make sure that the following requirements are met: + +* The `quarkus-extension.yaml` file (in the extension's `runtime/` module) has the minimum metadata set: +** `name` +** `description` (unless you have it already set in the `runtime/pom.xml`'s `` element, which is the recommended approach) + +* Your extension is published in Maven Central + +Then you must create a pull request adding a `your-extension.yaml` file in the `extensions/` directory in the link:https://github.com/quarkusio/quarkus-extension-catalog[Quarkus Extension Catalog]. The YAML must have the following structure: + +```yaml +group-id: +artifact-id: +``` + +That's all. Once the pull request is merged, a scheduled job will check Maven Central for new versions and update the link:extension-registry-user.adoc[Quarkus Extension Registry]. + diff --git a/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceConfigurationHandlerBuildItem.java b/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceConfigurationHandlerBuildItem.java index bf5266d69a7d3..0b246e4a123e6 100644 --- a/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceConfigurationHandlerBuildItem.java +++ b/extensions/datasource/deployment-spi/src/main/java/io/quarkus/datasource/deployment/spi/DevServicesDatasourceConfigurationHandlerBuildItem.java @@ -51,29 +51,39 @@ public Predicate getCheckConfiguredFunction() { public static DevServicesDatasourceConfigurationHandlerBuildItem jdbc(String dbKind) { return new DevServicesDatasourceConfigurationHandlerBuildItem(dbKind, new BiFunction>() { + @Override public Map apply(String dsName, DevServicesDatasourceProvider.RunningDevServicesDatasource runningDevDb) { if (dsName == null) { return Collections.singletonMap("quarkus.datasource.jdbc.url", runningDevDb.getUrl()); } else { - return Collections.singletonMap("quarkus.datasource.\"" + dsName + "\".jdbc.url", - runningDevDb.getUrl()); + // we use quoted and unquoted versions because depending on whether a user configured other JDBC properties + // one of the URLs may be ignored + // see https://github.com/quarkusio/quarkus/issues/21387 + return Map.of( + datasourceURLPropName(dsName), runningDevDb.getUrl(), + datasourceURLPropName("\"" + dsName + "\""), runningDevDb.getUrl()); } } + }, new Predicate() { @Override public boolean test(String dsName) { if (dsName == null) { return ConfigUtils.isPropertyPresent("quarkus.datasource.jdbc.url"); } else { - return ConfigUtils.isPropertyPresent("quarkus.datasource.\"" + dsName + "\".jdbc.url") || - ConfigUtils.isPropertyPresent("quarkus.datasource." + dsName + ".jdbc.url"); + return ConfigUtils.isPropertyPresent(datasourceURLPropName(dsName)) || + ConfigUtils.isPropertyPresent(datasourceURLPropName("\"" + dsName + "\"")); } } }); } + private static String datasourceURLPropName(String dsName) { + return String.format("quarkus.datasource.%s.jdbc.url", dsName); + } + public static DevServicesDatasourceConfigurationHandlerBuildItem reactive(String dbKind) { return new DevServicesDatasourceConfigurationHandlerBuildItem(dbKind, new BiFunction>() { diff --git a/extensions/hibernate-orm/deployment/src/test/resources/application-multiple-persistence-units-annotations.properties b/extensions/hibernate-orm/deployment/src/test/resources/application-multiple-persistence-units-annotations.properties index 6a404b89edf59..fb06774c6dbd9 100644 --- a/extensions/hibernate-orm/deployment/src/test/resources/application-multiple-persistence-units-annotations.properties +++ b/extensions/hibernate-orm/deployment/src/test/resources/application-multiple-persistence-units-annotations.properties @@ -1,11 +1,10 @@ quarkus.datasource.db-kind=h2 -quarkus.datasource.jdbc.url=jdbc:h2:mem:default +quarkus.datasource.jdbc.max-size=1 quarkus.datasource.users.db-kind=h2 -quarkus.datasource.users.jdbc.url=jdbc:h2:mem:users +quarkus.datasource.users.jdbc.min-size=2 quarkus.datasource.inventory.db-kind=h2 -quarkus.datasource.inventory.jdbc.url=jdbc:h2:mem:inventory quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect quarkus.hibernate-orm.database.generation=drop-and-create diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java index 3970870f72b53..08a0fc7090e89 100644 --- a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java @@ -1,11 +1,15 @@ package io.quarkus.jdbc.oracle.deployment; +import java.util.Collections; + import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.RemovedResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ExcludeConfigBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageAllowIncompleteClasspathBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.maven.dependency.GACT; /** * The Oracle JDBC driver includes a {@literal META-INF/native-image} which enables a set @@ -102,4 +106,10 @@ NativeImageAllowIncompleteClasspathBuildItem naughtyDriver() { return new NativeImageAllowIncompleteClasspathBuildItem("quarkus-jdbc-oracle"); } + @BuildStep + RemovedResourceBuildItem overrideSubstitutions() { + return new RemovedResourceBuildItem(GACT.fromString("com.oracle.database.jdbc:ojdbc11"), + Collections.singleton("oracle/nativeimage/Target_java_io_ObjectStreamClass.class")); + } + } diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java index d751d6498233e..90257b7efd485 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java @@ -113,11 +113,17 @@ public String getGrantType() { public String refreshTokenProperty = OidcConstants.REFRESH_TOKEN_VALUE; /** - * Refresh token property name in a token grant response + * Access token expiry property name in a token grant response */ @ConfigItem(defaultValue = OidcConstants.EXPIRES_IN) public String expiresInProperty = OidcConstants.EXPIRES_IN; + /** + * Refresh token expiry property name in a token grant response + */ + @ConfigItem(defaultValue = OidcConstants.REFRESH_EXPIRES_IN) + public String refreshExpiresInProperty = OidcConstants.REFRESH_EXPIRES_IN; + public Type getType() { return type; } @@ -149,6 +155,14 @@ public String getExpiresInProperty() { public void setExpiresInProperty(String expiresInProperty) { this.expiresInProperty = expiresInProperty; } + + public String getRefreshExpiresInProperty() { + return refreshExpiresInProperty; + } + + public void setRefreshExpiresInProperty(String refreshExpiresInProperty) { + this.refreshExpiresInProperty = refreshExpiresInProperty; + } } /** diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/Tokens.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/Tokens.java index 37ca13c3133c2..fa2a7989107ab 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/Tokens.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/Tokens.java @@ -12,14 +12,16 @@ public class Tokens { final private Long accessTokenExpiresAt; final private Long refreshTokenTimeSkew; final private String refreshToken; + final Long refreshTokenExpiresAt; final private JsonObject grantResponse; public Tokens(String accessToken, Long accessTokenExpiresAt, Duration refreshTokenTimeSkewDuration, String refreshToken, - JsonObject grantResponse) { + Long refreshTokenExpiresAt, JsonObject grantResponse) { this.accessToken = accessToken; this.accessTokenExpiresAt = accessTokenExpiresAt; this.refreshTokenTimeSkew = refreshTokenTimeSkewDuration == null ? null : refreshTokenTimeSkewDuration.getSeconds(); this.refreshToken = refreshToken; + this.refreshTokenExpiresAt = refreshTokenExpiresAt; this.grantResponse = grantResponse; } @@ -44,11 +46,11 @@ public Long getRefreshTokenTimeSkew() { } public boolean isAccessTokenExpired() { - if (accessTokenExpiresAt == null) { - return false; - } - final long nowSecs = System.currentTimeMillis() / 1000; - return nowSecs > accessTokenExpiresAt; + return isExpired(accessTokenExpiresAt); + } + + public boolean isRefreshTokenExpired() { + return isExpired(refreshTokenExpiresAt); } public boolean isAccessTokenWithinRefreshInterval() { @@ -58,4 +60,12 @@ public boolean isAccessTokenWithinRefreshInterval() { final long nowSecs = System.currentTimeMillis() / 1000; return nowSecs + refreshTokenTimeSkew > accessTokenExpiresAt; } + + private static boolean isExpired(Long expiresAt) { + if (expiresAt == null) { + return false; + } + final long nowSecs = System.currentTimeMillis() / 1000; + return nowSecs > expiresAt; + } } diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java index 89aa4137f2f19..c87c94768669b 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java @@ -124,20 +124,16 @@ private Tokens emitGrantTokens(HttpResponse resp, boolean refresh) { if (resp.statusCode() == 200) { LOG.debugf("%s OidcClient has %s the tokens", oidcConfig.getId().get(), (refresh ? "refreshed" : "acquired")); JsonObject json = resp.bodyAsJsonObject(); + // access token final String accessToken = json.getString(oidcConfig.grant.accessTokenProperty); + final Long accessTokenExpiresAt = getExpiresAtValue(accessToken, json.getValue(oidcConfig.grant.expiresInProperty)); + final String refreshToken = json.getString(oidcConfig.grant.refreshTokenProperty); - final Object expiresInValue = json.getValue(oidcConfig.grant.expiresInProperty); - Long accessTokenExpiresAt; - if (expiresInValue != null) { - long accessTokenExpiresIn = expiresInValue instanceof Number ? ((Number) expiresInValue).longValue() - : Long.parseLong(expiresInValue.toString()); - accessTokenExpiresAt = oidcConfig.absoluteExpiresIn ? accessTokenExpiresIn - : Instant.now().getEpochSecond() + accessTokenExpiresIn; - } else { - accessTokenExpiresAt = getExpiresJwtClaim(accessToken); - } + final Long refreshTokenExpiresAt = getExpiresAtValue(refreshToken, + json.getValue(oidcConfig.grant.refreshExpiresInProperty)); + return new Tokens(accessToken, accessTokenExpiresAt, oidcConfig.refreshTokenTimeSkew.orElse(null), refreshToken, - json); + refreshTokenExpiresAt, json); } else { String errorMessage = resp.bodyAsString(); LOG.debugf("%s OidcClient has failed to complete the %s grant request: status: %d, error message: %s", @@ -147,8 +143,19 @@ private Tokens emitGrantTokens(HttpResponse resp, boolean refresh) { } } - private static Long getExpiresJwtClaim(String accessToken) { - JsonObject claims = decodeJwtToken(accessToken); + private Long getExpiresAtValue(String token, Object expiresInValue) { + if (expiresInValue != null) { + long tokenExpiresIn = expiresInValue instanceof Number ? ((Number) expiresInValue).longValue() + : Long.parseLong(expiresInValue.toString()); + return oidcConfig.absoluteExpiresIn ? tokenExpiresIn + : Instant.now().getEpochSecond() + tokenExpiresIn; + } else { + return token != null ? getExpiresJwtClaim(token) : null; + } + } + + private static Long getExpiresJwtClaim(String token) { + JsonObject claims = decodeJwtToken(token); if (claims != null) { try { return claims.getLong(Claims.exp.name()); diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java index 0060428bfd970..659c243c7f20a 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java @@ -1,7 +1,6 @@ package io.quarkus.oidc.client.runtime; import java.io.IOException; -import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.function.BiFunction; @@ -34,7 +33,6 @@ public class OidcClientRecorder { private static final Logger LOG = Logger.getLogger(OidcClientRecorder.class); private static final String DEFAULT_OIDC_CLIENT_ID = "Default"; - private static final Duration CONNECTION_BACKOFF_DURATION = Duration.ofSeconds(2); public OidcClients setup(OidcClientsConfig oidcClientsConfig, TlsConfig tlsConfig, Supplier vertx) { diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/TokensHelper.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/TokensHelper.java index 67c0537598171..ed41d5082e3b3 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/TokensHelper.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/TokensHelper.java @@ -40,9 +40,10 @@ public Uni getTokens(OidcClient oidcClient) { } else { Tokens tokens = currentState.tokens; if (tokens.isAccessTokenExpired() || tokens.isAccessTokenWithinRefreshInterval()) { - newState = new TokenRequestState(prepareUni(tokens.getRefreshToken() != null - ? oidcClient.refreshTokens(tokens.getRefreshToken()) - : oidcClient.getTokens())); + newState = new TokenRequestState( + prepareUni((tokens.getRefreshToken() != null && !tokens.isRefreshTokenExpired()) + ? oidcClient.refreshTokens(tokens.getRefreshToken()) + : oidcClient.getTokens())); if (tokenRequestStateUpdater.compareAndSet(this, currentState, newState)) { return newState.tokenUni; } 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 feb20245ffd24..a023d7d4bbcff 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 @@ -44,4 +44,5 @@ public final class OidcConstants { public static final String EXCHANGE_GRANT = "urn:ietf:params:oauth:grant-type:token-exchange"; public static final String EXPIRES_IN = "expires_in"; + public static final String REFRESH_EXPIRES_IN = "refresh_expires_in"; } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index 16315fee70bc8..11c4ea79e0a8e 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -25,6 +25,8 @@ import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.oidc.SecurityEvent; +import io.quarkus.oidc.TokenIntrospectionCache; +import io.quarkus.oidc.UserInfoCache; import io.quarkus.oidc.runtime.DefaultTenantConfigResolver; import io.quarkus.oidc.runtime.DefaultTokenIntrospectionUserInfoCache; import io.quarkus.oidc.runtime.DefaultTokenStateManager; @@ -95,7 +97,7 @@ public SyntheticBeanBuildItem addDefaultCacheBean(OidcConfig config, OidcRecorder recorder, CoreVertxBuildItem vertxBuildItem) { return SyntheticBeanBuildItem.configure(DefaultTokenIntrospectionUserInfoCache.class).unremovable() - .types(DefaultTokenIntrospectionUserInfoCache.class) + .types(DefaultTokenIntrospectionUserInfoCache.class, TokenIntrospectionCache.class, UserInfoCache.class) .supplier(recorder.setupTokenCache(config, vertxBuildItem.getVertx())) .scope(Singleton.class) .setRuntimeInit() diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java index 84d2caca749de..96d4f3482a041 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java @@ -30,7 +30,9 @@ public class OidcConfig { public Map namedTenants; /** - * Default TokenIntrospection and UserInfo Cache configuration which is used for all the tenants if it is enabled. + * Default TokenIntrospection and UserInfo Cache configuration which is used for all the tenants if it is enabled + * with the build-time 'quarkus.oidc.default-token-cache-enabled' property ('true' by default) and also activated, + * see its `max-size` property. */ @ConfigItem public TokenCache tokenCache = new TokenCache(); 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 36da33973b1e1..b012cd16036ab 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 @@ -258,6 +258,7 @@ protected static Uni getJsonWebSetUni(OidcProviderClient client, .expireIn(connectionDelayInMillisecs) .onFailure() .transform(t -> toOidcException(t, oidcConfig.authServerUrl.get())) + .onFailure() .invoke(client::close); } else { return client.getJsonWebKeySet(); diff --git a/extensions/panache/mongodb-panache-common/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/BasePanacheMongoResourceProcessor.java b/extensions/panache/mongodb-panache-common/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/BasePanacheMongoResourceProcessor.java index f7a190cf0273b..57cd219e5a12a 100644 --- a/extensions/panache/mongodb-panache-common/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/BasePanacheMongoResourceProcessor.java +++ b/extensions/panache/mongodb-panache-common/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/BasePanacheMongoResourceProcessor.java @@ -42,6 +42,7 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem; import io.quarkus.deployment.util.JandexUtil; import io.quarkus.gizmo.DescriptorUtils; import io.quarkus.jackson.spi.JacksonModuleBuildItem; @@ -68,8 +69,8 @@ public abstract class BasePanacheMongoResourceProcessor { public static final DotName BSON_IGNORE = createSimple(BsonIgnore.class.getName()); public static final DotName BSON_PROPERTY = createSimple(BsonProperty.class.getName()); public static final DotName MONGO_ENTITY = createSimple(io.quarkus.mongodb.panache.common.MongoEntity.class.getName()); - public static final DotName OBJECT_ID = createSimple(ObjectId.class.getName()); public static final DotName PROJECTION_FOR = createSimple(io.quarkus.mongodb.panache.common.ProjectionFor.class.getName()); + public static final String BSON_PACKAGE = "org.bson."; @BuildStep public void buildImperative(CombinedIndexBuildItem index, @@ -381,8 +382,8 @@ protected void processTypes(CombinedIndexBuildItem index, } @BuildStep - ReflectiveClassBuildItem registerForReflection() { - return new ReflectiveClassBuildItem(true, true, OBJECT_ID.toString()); + ReflectiveHierarchyIgnoreWarningBuildItem ignoreBsonTypes() { + return new ReflectiveHierarchyIgnoreWarningBuildItem(dotname -> dotname.toString().startsWith(BSON_PACKAGE)); } @BuildStep diff --git a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/visitors/KotlinPanacheClassOperationGenerationVisitor.java b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/visitors/KotlinPanacheClassOperationGenerationVisitor.java index c1cdd463b82a2..c2f035b45aeaf 100644 --- a/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/visitors/KotlinPanacheClassOperationGenerationVisitor.java +++ b/extensions/panache/panache-common/deployment/src/main/java/io/quarkus/panache/common/deployment/visitors/KotlinPanacheClassOperationGenerationVisitor.java @@ -64,6 +64,7 @@ public class KotlinPanacheClassOperationGenerationVisitor extends ClassVisitor { public static final String NULLABLE_DESCRIPTOR = "Lorg/jetbrains/annotations/Nullable;"; public static final ByteCodeType OBJECT = new ByteCodeType(Object.class); protected static final ByteCodeType CLASS = new ByteCodeType(Class.class); + private static final String CTOR_METHOD_NAME = ""; protected final Function argMapper; protected final ClassInfo classInfo; protected final ByteCodeType entityUpperBound; @@ -291,7 +292,7 @@ private void emitNullCheck(MethodVisitor mv, Type returnType) { .replace("java.util.List", "kotlin.collections.List"))); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/NullPointerException", - "", "(Ljava/lang/String;)V", false); + CTOR_METHOD_NAME, "(Ljava/lang/String;)V", false); mv.visitInsn(ATHROW); mv.visitLabel(label); mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] { "java/lang/Object" }); @@ -538,6 +539,9 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str } else if (name.contains("$")) { //some agents such as jacoco add new methods, they generally have $ in the name return super.visitMethod(access, name, descriptor, signature, exceptions); + } else if (name.equals(CTOR_METHOD_NAME)) { + //Arc can add no-args constructors to support intercepted beans + return super.visitMethod(access, name, descriptor, signature, exceptions); } return null; } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java index 1869375cda46e..6ae56338da2a3 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/devmode/QuteErrorPageSetup.java @@ -43,15 +43,15 @@ public class QuteErrorPageSetup implements HotReplacementSetup { + "{#if realLines.get(0) > 1}...
{/if}\n" + "{#for line in sourceLines}\n" // highlight the error line - start - + "{#if lineNumber is realLines.get(index)}
{/if}\n" + + "{#if lineNumber is realLines.get(line_index)}
{/if}\n" // line number - + "{realLines.get(index).pad}\n" + + "{realLines.get(line_index).pad}\n" // line content + " {line}\n" // highlight the error line - end - + "{#if lineNumber is realLines.get(index)}
{#else}
{/if}\n" + + "{#if lineNumber is realLines.get(line_index)}
{#else}
{/if}\n" // point to error - + "{#if lineNumber is realLines.get(index)}{space.pad}{#for i in lineCharacterStart}={/for}^
{/if}\n" + + "{#if lineNumber is realLines.get(line_index)}{space.pad}{#for i in lineCharacterStart}={/for}^
{/if}\n" + "{/for}\n" + "{#if endLinesSkipped}...{/if}\n" + ""; diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxBlockingOutput.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxBlockingOutput.java index 4a84d8dd80c36..d974474bd265b 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxBlockingOutput.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxBlockingOutput.java @@ -79,6 +79,7 @@ public void write(ByteBuf data, boolean last) throws IOException { if (data != null && data.refCnt() > 0) { data.release(); } + request.response().end(); throw new IOException(throwable); } try { diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxHttpResponse.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxHttpResponse.java index d644c720b5881..5e5440491e2f1 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxHttpResponse.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxHttpResponse.java @@ -138,7 +138,7 @@ private void transformHeadersList(final String key, final List valueList public void finish() throws IOException { checkException(); - if (finished || response.ended()) + if (finished || response.ended() || response.closed()) return; try { if (os != null) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java index 087095f756e7b..b1d07026a158e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java @@ -495,6 +495,11 @@ private static ResultHandle[] getResponseFilterResultHandles(MethodInfo targetMe ResultHandle routingContextHandle = GeneratorUtils.routingContextHandler(filterMethod, rrReqCtxHandle); targetMethodParamHandles[i] = filterMethod.invokeInterfaceMethod( ofMethod(RoutingContext.class, "response", HttpServerResponse.class), routingContextHandle); + } else if (URI_INFO.equals(paramDotName)) { + GeneratorUtils.paramHandleFromReqContextMethod(filterMethod, rrReqCtxHandle, targetMethodParamHandles, + i, + "getUriInfo", + URI_INFO); } else if (RESOURCE_INFO.equals(paramDotName)) { targetMethodParamHandles[i] = getResourceInfoHandle(filterMethod, rrReqCtxHandle); } else if (SIMPLIFIED_RESOURCE_INFO.equals(paramDotName)) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/resources/dev-templates/scores.html b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/resources/dev-templates/scores.html index 0cba8e7850b77..c7ddf5d9936d4 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/resources/dev-templates/scores.html +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/resources/dev-templates/scores.html @@ -205,7 +205,7 @@ {/if} {#if !endpoint.requestFilterEntries.isEmpty()} -
+
Filters:
diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/CustomContainerResponseFilter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/CustomContainerResponseFilter.java index 5490af21c2b1e..c45a1d5430757 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/CustomContainerResponseFilter.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/CustomContainerResponseFilter.java @@ -1,10 +1,12 @@ package io.quarkus.resteasy.reactive.server.test.customproviders; +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 javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.core.UriInfo; import org.jboss.resteasy.reactive.server.ServerResponseFilter; import org.jboss.resteasy.reactive.server.SimpleResourceInfo; @@ -15,10 +17,11 @@ public class CustomContainerResponseFilter { @ServerResponseFilter public void whatever(SimpleResourceInfo simplifiedResourceInfo, ContainerResponseContext responseContext, - ContainerRequestContext requestContext, Throwable t) { + ContainerRequestContext requestContext, UriInfo uriInfo, Throwable t) { assertTrue( PreventAbortResteasyReactiveContainerRequestContext.class.isAssignableFrom(requestContext.getClass())); assertNull(t); + assertNotNull(uriInfo); if (simplifiedResourceInfo != null) { responseContext.getHeaders().putSingle("java-method", simplifiedResourceInfo.getMethodName()); } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/MediaTypeSuffixTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/MediaTypeSuffixTest.java new file mode 100644 index 0000000000000..87eeae28e6a13 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/MediaTypeSuffixTest.java @@ -0,0 +1,69 @@ +package io.quarkus.rest.client.reactive; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.is; + +import java.util.Map; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class MediaTypeSuffixTest { + + private static final String CUSTOM_JSON_MEDIA_TYPE = "application/vnd.search.v1+json"; + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(HelloResource.class, Client.class)) + .withConfigurationResource("media-type-suffix-application.properties"); + + @Test + public void test() { + when() + .get("/hello") + .then() + .statusCode(200) + .body("foo", is("bar")); + } + + @RegisterRestClient(configKey = "test") + @Path("/hello") + public interface Client { + + @GET + @Path("/custom") + @Produces(CUSTOM_JSON_MEDIA_TYPE) + Map test(); + } + + @Path("/hello") + public static class HelloResource { + + private final Client client; + + public HelloResource(@RestClient Client client) { + this.client = client; + } + + @GET + @Path("/custom") + @Produces(CUSTOM_JSON_MEDIA_TYPE) + public Map hello() { + return Map.of("foo", "bar"); + } + + @GET + public Map test() { + return client.test(); + } + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/media-type-suffix-application.properties b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/media-type-suffix-application.properties new file mode 100644 index 0000000000000..f37c300de6005 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/media-type-suffix-application.properties @@ -0,0 +1 @@ +quarkus.rest-client.test.url=http://localhost:${quarkus.http.test-port:8081} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java index 7e08b1f9d3af1..59ed540f1c249 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java @@ -477,9 +477,12 @@ private void flushTag() { // Parameter declaration // {@org.acme.Foo foo} Scope currentScope = scopeStack.peek(); - int spaceIdx = content.indexOf(" "); - String key = content.substring(spaceIdx + 1, content.length()); - String value = content.substring(1, spaceIdx); + String[] parts = content.substring(1).trim().split("[ ]{1,}"); + if (parts.length != 2) { + throw parserError("invalid parameter declaration " + START_DELIMITER + buffer.toString() + END_DELIMITER); + } + String value = parts[0]; + String key = parts[1]; currentScope.putBinding(key, Expressions.typeInfoFrom(value)); sectionStack.peek().currentBlock().addNode(new ParameterDeclarationNode(content, origin(0))); } else { diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java index aba4d4f4e6357..1a12c2952f0c3 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java @@ -358,6 +358,20 @@ public void testInvalidBracket() { "Parser error on line 1: invalid bracket notation expression in {foo.baz[}", 1); } + @Test + public void testInvalidParamDeclaration() { + assertParserError("{@com.foo }", + "Parser error on line 1: invalid parameter declaration {@com.foo }", 1); + assertParserError("{@ com.foo }", + "Parser error on line 1: invalid parameter declaration {@ com.foo }", 1); + assertParserError("{@com.foo.Bar bar baz}", + "Parser error on line 1: invalid parameter declaration {@com.foo.Bar bar baz}", 1); + assertParserError("{@}", + "Parser error on line 1: invalid parameter declaration {@}", 1); + assertParserError("{@\n}", + "Parser error on line 1: invalid parameter declaration {@\n}", 1); + } + public static class Foo { public List getItems() { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java index 55815186fa959..c73f288f0c87a 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java @@ -17,6 +17,7 @@ import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl; import org.jboss.resteasy.reactive.common.util.CaseInsensitiveMap; +import org.jboss.resteasy.reactive.common.util.MediaTypeHelper; public class ClientReaderInterceptorContextImpl extends AbstractClientInterceptorContextImpl implements ReaderInterceptorContext { @@ -34,7 +35,7 @@ public ClientReaderInterceptorContextImpl(Annotation[] annotations, Class ent Map properties, MultivaluedMap headers, ConfigurationImpl configuration, Serialisers serialisers, InputStream inputStream, ReaderInterceptor[] interceptors) { - super(annotations, entityClass, entityType, mediaType, properties); + super(annotations, entityClass, entityType, MediaTypeHelper.withSuffixAsSubtype(mediaType), properties); this.configuration = configuration; this.serialisers = serialisers; this.inputStream = inputStream; diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/MediaTypeHelper.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/MediaTypeHelper.java index 422de499dd6e3..41b72877f9808 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/MediaTypeHelper.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/MediaTypeHelper.java @@ -272,6 +272,9 @@ public static boolean isUnsupportedWildcardSubtype(MediaType mediaType) { * that uses the suffix as the subtype */ public static MediaType withSuffixAsSubtype(MediaType mediaType) { + if (mediaType == null) { + return null; + } int plusIndex = mediaType.getSubtype().indexOf('+'); if ((plusIndex > -1) && (plusIndex < mediaType.getSubtype().length() - 1)) { mediaType = new MediaType(mediaType.getType(), diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerResponseFilter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerResponseFilter.java index daf7fa44c1e1c..4247098221019 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerResponseFilter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerResponseFilter.java @@ -8,6 +8,7 @@ import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.UriInfo; /** * When used on a method, then an implementation of {@link javax.ws.rs.container.ContainerResponseContext} is generated @@ -41,6 +42,7 @@ *
  • {@link ContainerRequestContext} *
  • {@link ContainerResponseContext} *
  • {@link ResourceInfo} + *
  • {@link UriInfo} *
  • {@link SimpleResourceInfo} *
  • {@link Throwable} - The thrown exception - or {@code null} if no exception was thrown * diff --git a/integration-tests/devmode/pom.xml b/integration-tests/devmode/pom.xml index a279fdddaf513..5fedcbc12fe25 100644 --- a/integration-tests/devmode/pom.xml +++ b/integration-tests/devmode/pom.xml @@ -36,7 +36,7 @@ io.quarkus - quarkus-qute-deployment + quarkus-resteasy-reactive-qute-deployment test diff --git a/integration-tests/devmode/src/test/java/io/quarkus/test/qute/QuteErrorPageTest.java b/integration-tests/devmode/src/test/java/io/quarkus/test/qute/QuteErrorPageTest.java new file mode 100644 index 0000000000000..be9814850f204 --- /dev/null +++ b/integration-tests/devmode/src/test/java/io/quarkus/test/qute/QuteErrorPageTest.java @@ -0,0 +1,29 @@ +package io.quarkus.test.qute; + +import static org.hamcrest.CoreMatchers.containsString; + +import javax.ws.rs.core.Response.Status; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class QuteErrorPageTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withApplicationRoot( + root -> root.addAsResource(new StringAsset("{hello.foo}"), "templates/hello.txt")); + + @Test + public void testErrorPage() { + config.modifyResourceFile("templates/hello.txt", file -> "{@java.lang.String hello}{hello.foo}"); + RestAssured.when().get("/hello").then() + .body(containsString("Incorrect expression found: {hello.foo}")) + .statusCode(Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + +} diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/QuarkusPluginFunctionalTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/QuarkusPluginFunctionalTest.java index bfc8c6d251456..fa25471402d78 100644 --- a/integration-tests/gradle/src/test/java/io/quarkus/gradle/QuarkusPluginFunctionalTest.java +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/QuarkusPluginFunctionalTest.java @@ -151,6 +151,16 @@ public void canRunTest() throws Exception { assertThat(buildResult.getTasks().get(":test")).isEqualTo(BuildResult.SUCCESS_OUTCOME); } + @Test + public void generateCodeBeforeTests() throws Exception { + createProject(SourceType.JAVA); + + BuildResult firstBuild = runGradleWrapper(projectRoot, "test", "--stacktrace"); + assertThat(firstBuild.getOutput()).contains("Task :quarkusGenerateCode"); + assertThat(firstBuild.getOutput()).contains("Task :quarkusGenerateCodeTests"); + assertThat(firstBuild.getTasks().get(":test")).isEqualTo(BuildResult.SUCCESS_OUTCOME); + } + private void createProject(SourceType sourceType) throws Exception { Map context = new HashMap<>(); context.put("path", "/greeting"); diff --git a/integration-tests/hibernate-orm-panache-kotlin/src/main/kotlin/io/quarkus/it/panache/kotlin/AddressDao.kt b/integration-tests/hibernate-orm-panache-kotlin/src/main/kotlin/io/quarkus/it/panache/kotlin/AddressDao.kt index 3193271864633..f10ac5da7b6e5 100644 --- a/integration-tests/hibernate-orm-panache-kotlin/src/main/kotlin/io/quarkus/it/panache/kotlin/AddressDao.kt +++ b/integration-tests/hibernate-orm-panache-kotlin/src/main/kotlin/io/quarkus/it/panache/kotlin/AddressDao.kt @@ -4,7 +4,7 @@ import io.quarkus.hibernate.orm.panache.kotlin.PanacheRepositoryBase import javax.enterprise.context.ApplicationScoped @ApplicationScoped -open class AddressDao : PanacheRepositoryBase { +open class AddressDao(private val dummyService: DummyService) : PanacheRepositoryBase { companion object { fun shouldBeOverridden(): Nothing { throw UnsupportedOperationException("this should be called and not be overwritten by the quarkus plugin") @@ -12,4 +12,4 @@ open class AddressDao : PanacheRepositoryBase { } override fun count(query: String, params: Map): Long = shouldBeOverridden() -} \ No newline at end of file +} diff --git a/integration-tests/hibernate-orm-panache-kotlin/src/main/kotlin/io/quarkus/it/panache/kotlin/DummyService.kt b/integration-tests/hibernate-orm-panache-kotlin/src/main/kotlin/io/quarkus/it/panache/kotlin/DummyService.kt new file mode 100644 index 0000000000000..85c330413fdb7 --- /dev/null +++ b/integration-tests/hibernate-orm-panache-kotlin/src/main/kotlin/io/quarkus/it/panache/kotlin/DummyService.kt @@ -0,0 +1,8 @@ +package io.quarkus.it.panache.kotlin + +import javax.enterprise.context.ApplicationScoped + +// used only to validate that we can inject CDI beans into Panache repositories written in Kotlin +@ApplicationScoped +class DummyService { +} diff --git a/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/SerializationTestEndpoint.java b/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/SerializationTestEndpoint.java new file mode 100644 index 0000000000000..f7ceff23e79ab --- /dev/null +++ b/integration-tests/jpa-oracle/src/main/java/io/quarkus/example/jpaoracle/SerializationTestEndpoint.java @@ -0,0 +1,41 @@ +package io.quarkus.example.jpaoracle; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet(name = "JPATestOracleSerialization", urlPatterns = "/jpa-oracle/testserialization") +public class SerializationTestEndpoint extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + try { + final String output = serializedstring(); + resp.getWriter().write(output); + + } catch (Exception e) { + resp.getWriter().write("An error occurred while attempting serialization operations"); + } + } + + private String serializedstring() throws IOException, ClassNotFoundException { + byte[] bytes = null; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject("Hello from Serialization Test"); + oos.flush(); + bytes = baos.toByteArray(); + } + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + return (String) ois.readObject(); + } + } +} diff --git a/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/JPAFunctionalityTest.java b/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/JPAFunctionalityTest.java index b871fd14d1bfa..ad3fe68f84c7c 100644 --- a/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/JPAFunctionalityTest.java +++ b/integration-tests/jpa-oracle/src/test/java/io/quarkus/it/jpa/oracle/JPAFunctionalityTest.java @@ -20,4 +20,9 @@ public void testJPAFunctionalityFromServlet() throws Exception { RestAssured.when().get("/jpa-oracle/testfunctionality").then().body(is("OK")); } + @Test + public void testSerializationFromServlet() throws Exception { + RestAssured.when().get("/jpa-oracle/testserialization").then().body(is("Hello from Serialization Test")); + } + } diff --git a/integration-tests/mongodb-panache-kotlin/src/main/kotlin/io/quarkus/it/mongodb/panache/book/BookRepository.kt b/integration-tests/mongodb-panache-kotlin/src/main/kotlin/io/quarkus/it/mongodb/panache/book/BookRepository.kt index c2d2bebd4c4e6..30e6438eac573 100644 --- a/integration-tests/mongodb-panache-kotlin/src/main/kotlin/io/quarkus/it/mongodb/panache/book/BookRepository.kt +++ b/integration-tests/mongodb-panache-kotlin/src/main/kotlin/io/quarkus/it/mongodb/panache/book/BookRepository.kt @@ -4,4 +4,4 @@ import io.quarkus.mongodb.panache.kotlin.PanacheMongoRepository import javax.enterprise.context.ApplicationScoped @ApplicationScoped -class BookRepository : PanacheMongoRepository \ No newline at end of file +class BookRepository(private val dummyService: DummyService) : PanacheMongoRepository diff --git a/integration-tests/mongodb-panache-kotlin/src/main/kotlin/io/quarkus/it/mongodb/panache/book/DummyService.kt b/integration-tests/mongodb-panache-kotlin/src/main/kotlin/io/quarkus/it/mongodb/panache/book/DummyService.kt new file mode 100644 index 0000000000000..eed33d52a1ba8 --- /dev/null +++ b/integration-tests/mongodb-panache-kotlin/src/main/kotlin/io/quarkus/it/mongodb/panache/book/DummyService.kt @@ -0,0 +1,8 @@ +package io.quarkus.it.mongodb.panache.book + +import javax.enterprise.context.ApplicationScoped + +// used only to validate that we can inject CDI beans into Panache repositories written in Kotlin +@ApplicationScoped +class DummyService { +} diff --git a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java index a2ca7c1211688..ea8b059a72201 100644 --- a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -50,7 +50,7 @@ public Map start() { .aResponse() .withHeader("Content-Type", MediaType.APPLICATION_JSON) .withBody( - "{\"access_token\":\"access_token_2\", \"expires_in\":4, \"refresh_token\":\"refresh_token_1\"}"))); + "{\"access_token\":\"access_token_2\", \"expires_in\":4, \"refresh_token\":\"refresh_token_2\", \"refresh_expires_in\":1}"))); server.stubFor(WireMock.post("/refresh-token-only") .withRequestBody(matching("grant_type=refresh_token&refresh_token=shared_refresh_token")) diff --git a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java index f5a955b8549a4..0a274ca502f40 100644 --- a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java +++ b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java @@ -30,12 +30,41 @@ public class OidcClientTest { @Test public void testEchoAndRefreshTokens() { + // access_token_1 and refresh_token_1 are acquired using a password grant request. + // access_token_1 expires in 4 seconds, refresh_token_1 has no lifespan limit as no `refresh_expires_in` property is returned. + // "Default OidcClient has acquired the tokens" record is added to the log RestAssured.when().get("/frontend/echoToken") .then() .statusCode(200) .body(equalTo("access_token_1")); - // Wait until the access token has expired + // Wait until the access_token_1 has expired + waitUntillAccessTokenHasExpired(); + + // access_token_1 has expired, refresh_token_1 is assumed to be valid and used to acquire access_token_2 and refresh_token_2. + // access_token_2 expires in 4 seconds, but refresh_token_2 - in 1 sec - it will expire by the time access_token_2 has expired + // "Default OidcClient has refreshed the tokens" record is added to the log + RestAssured.when().get("/frontend/echoToken") + .then() + .statusCode(200) + .body(equalTo("access_token_2")); + + // Wait until the access_token_2 has expired + waitUntillAccessTokenHasExpired(); + + // Both access_token_2 and refresh_token_2 have now expired therefore a password grant request is repeated, + // as opposed to using a refresh token grant. + // access_token_1 is returned again - as the same token URL and grant properties are used and Wiremock stub returns access_token_1 + // 2nd "Default OidcClient has acquired the tokens" record is added to the log + RestAssured.when().get("/frontend/echoToken") + .then() + .statusCode(200) + .body(equalTo("access_token_1")); + + checkLog(); + } + + private static void waitUntillAccessTokenHasExpired() { long expiredTokenTime = System.currentTimeMillis() + 5000; await().atMost(10, TimeUnit.SECONDS) .pollInterval(Duration.ofSeconds(3)) @@ -45,12 +74,6 @@ public Boolean call() throws Exception { return System.currentTimeMillis() > expiredTokenTime; } }); - - RestAssured.when().get("/frontend/echoToken") - .then() - .statusCode(200) - .body(equalTo("access_token_2")); - checkLog(); } @Test @@ -109,8 +132,8 @@ public void run() throws Throwable { } } - assertEquals(1, tokenAcquisitionCount, - "Log file must contain a single OidcClientImpl token acquisition confirmation"); + assertEquals(2, tokenAcquisitionCount, + "Log file must contain two OidcClientImpl token acquisition confirmations"); assertEquals(1, tokenRefreshedCount, "Log file must contain a single OidcClientImpl token refresh confirmation"); } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowResource.java index cd68880ea8552..e11031259b77e 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowResource.java @@ -4,6 +4,7 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; +import io.quarkus.oidc.runtime.DefaultTokenIntrospectionUserInfoCache; import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; @@ -14,8 +15,11 @@ public class CodeFlowResource { @Inject SecurityIdentity identity; + @Inject + DefaultTokenIntrospectionUserInfoCache tokenCache; + @GET public String access() { - return identity.getPrincipal().getName(); + return identity.getPrincipal().getName() + ", cache size: " + tokenCache.getCacheSize(); } } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java index 90e2ced96f7ba..987850cb62ad1 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java @@ -5,6 +5,7 @@ import javax.ws.rs.Path; import io.quarkus.oidc.UserInfo; +import io.quarkus.oidc.runtime.DefaultTokenIntrospectionUserInfoCache; import io.quarkus.security.Authenticated; @Path("/code-flow-user-info") @@ -14,8 +15,14 @@ public class CodeFlowUserInfoResource { @Inject UserInfo userInfo; + @Inject + DefaultTokenIntrospectionUserInfoCache tokenCache; + @GET public String access() { - return userInfo.getString("preferred_username"); + int cacheSize = tokenCache.getCacheSize(); + tokenCache.clearCache(); + return userInfo.getString("preferred_username") + ", cache size: " + + cacheSize; } } diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 347269c186f0a..1b76d8fb9ec01 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -27,11 +27,15 @@ quarkus.oidc.code-flow-user-info-only.client-id=quarkus-web-app quarkus.oidc.code-flow-user-info-only.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow quarkus.oidc.code-flow-user-info-only.application-type=web-app +quarkus.oidc.token-cache.max-size=1 + quarkus.oidc.bearer.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.bearer.client-id=quarkus-app quarkus.oidc.bearer.credentials.secret=secret quarkus.oidc.bearer.authentication.scopes=profile,email,phone quarkus.oidc.bearer.token.audience=https://service.example.com +quarkus.oidc.bearer.token.audience=https://service.example.com +quarkus.oidc.bearer.allow-token-introspection-cache=false quarkus.oidc.bearer-no-introspection.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.bearer-no-introspection.client-id=quarkus-app diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 68d365b2ada44..ae4d1dac27b19 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -39,7 +39,7 @@ public void testCodeFlow() throws IOException { page = form.getInputByValue("login").click(); - assertEquals("alice", page.getBody().asText()); + assertEquals("alice, cache size: 0", page.getBody().asText()); } } @@ -56,7 +56,7 @@ public void testCodeFlowUserInfo() throws IOException { page = form.getInputByValue("login").click(); - assertEquals("alice", page.getBody().asText()); + assertEquals("alice, cache size: 1", page.getBody().asText()); } } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java index cd0e50dd00529..ddc0a3a2e6a58 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java @@ -74,33 +74,6 @@ public void start() throws IOException { } } - if (devServicesLaunchResult.manageNetwork() && (devServicesLaunchResult.networkId() != null)) { - try { - int networkCreateResult = new ProcessBuilder().redirectError(DISCARD).redirectOutput(DISCARD) - .command(DOCKER_BINARY, "network", "create", devServicesLaunchResult.networkId()).start().waitFor(); - if (networkCreateResult > 0) { - throw new RuntimeException("Creating container network '" + devServicesLaunchResult.networkId() - + "' completed unsuccessfully"); - } - // do the cleanup in a shutdown hook because there might be more services (launched via QuarkusTestResourceLifecycleManager) connected to the network - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { - @Override - public void run() { - try { - new ProcessBuilder().redirectError(DISCARD).redirectOutput(DISCARD) - .command(DOCKER_BINARY, "network", "rm", devServicesLaunchResult.networkId()).start() - .waitFor(); - } catch (InterruptedException | IOException ignored) { - System.out.println( - "Unable to delete container network '" + devServicesLaunchResult.networkId() + "'"); - } - } - })); - } catch (InterruptedException e) { - throw new RuntimeException("Unable to pull container image '" + containerImage + "'", e); - } - } - System.setProperty("test.url", TestHTTPResourceManager.getUri()); if (httpPort == 0) { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index 36dd9ee032936..b253d9d039cdb 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -2,6 +2,7 @@ import static io.quarkus.test.common.PathTestHelper.getAppClassLocationForTestLocation; import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; +import static java.lang.ProcessBuilder.Redirect.DISCARD; import java.io.File; import java.io.FileInputStream; @@ -58,6 +59,8 @@ public final class IntegrationTestUtil { public static final int DEFAULT_HTTPS_PORT = 8444; public static final long DEFAULT_WAIT_TIME_SECONDS = 60; + private static final String DOCKER_BINARY = "docker"; + private IntegrationTestUtil() { } @@ -304,7 +307,43 @@ public void accept(String s, String s2) { } } - return new DefaultDevServicesLaunchResult(propertyMap, networkId, manageNetwork, curatedApplication); + DefaultDevServicesLaunchResult result = new DefaultDevServicesLaunchResult(propertyMap, networkId, manageNetwork, + curatedApplication); + createNetworkIfNecessary(result); + return result; + } + + // this probably isn't the best place for this method, but we need to create the docker container before + // user code is aware of the network + private static void createNetworkIfNecessary( + final ArtifactLauncher.InitContext.DevServicesLaunchResult devServicesLaunchResult) { + if (devServicesLaunchResult.manageNetwork() && (devServicesLaunchResult.networkId() != null)) { + try { + int networkCreateResult = new ProcessBuilder().redirectError(DISCARD).redirectOutput(DISCARD) + .command(DOCKER_BINARY, "network", "create", devServicesLaunchResult.networkId()).start().waitFor(); + if (networkCreateResult > 0) { + throw new RuntimeException("Creating container network '" + devServicesLaunchResult.networkId() + + "' completed unsuccessfully"); + } + // do the cleanup in a shutdown hook because there might be more services (launched via QuarkusTestResourceLifecycleManager) connected to the network + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + try { + new ProcessBuilder().redirectError(DISCARD).redirectOutput(DISCARD) + .command(DOCKER_BINARY, "network", "rm", devServicesLaunchResult.networkId()).start() + .waitFor(); + } catch (InterruptedException | IOException ignored) { + System.out.println( + "Unable to delete container network '" + devServicesLaunchResult.networkId() + "'"); + } + } + })); + } catch (Exception e) { + throw new RuntimeException("Creating container network '" + devServicesLaunchResult.networkId() + + "' completed unsuccessfully"); + } + } } static void activateLogging() {