diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74183fd8497d3..b0924e3f2997a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -400,6 +400,53 @@ Thus, it is recommended to use the following approach: Due to Quarkus being a large repository, having to rebuild the entire project every time a change is made isn't very productive. The following Maven tips can vastly speed up development when working on a specific extension. +#### Using mvnd + +[mvnd](https://github.com/apache/maven-mvnd) is a daemon for Maven providing faster builds. +It parallelizes your builds by default and makes sure the output is consistent even for a parallelized build. + +You can https://github.com/apache/maven-mvnd?tab=readme-ov-file#how-to-install-mvnd[install mvnd] with SDKMAN!, Homebrew... + +mvnd is a good companion for your Quarkus builds. + +Make sure you install the latest mvnd 1.0.x which embeds Maven 3.x as Quarkus does not support Maven 4 yet. +Once it is installed, you can use `mvnd` in your Maven command lines instead of the typical `mvn` or `./mvnw`. + +If anything goes wrong, you can stop the daemon and start fresh with `mvnd --stop`. + +#### Using aliases + +While building with `-Dquickly` or `-DquicklyDocs` is practical when contributing your first patches, +if you contribute to Quarkus often, it is recommended to have your own aliases - for instance to make sure your build is parallelized. + +Here are a couple of useful aliases that are good starting points - and that you will need to adapt to your environment: + +- `build-fast`: build the Quarkus artifacts and install them +- `build-docs`: run from the root of the project, build the documentation +- `format`: format the source code following our coding conventions +- `qss`: run the Quarkus CLI from a snapshot (make sure you build the artifacts first) + +- If using mvnd + +```sh +alias build-fast="mvnd -e -DskipDocs -DskipTests -DskipITs -Dinvoker.skip -DskipExtensionValidation -Dskip.gradle.tests -Dtruststore.skip clean install" +alias build-docs="mvnd -e -DskipTests -DskipITs -Dinvoker.skip -DskipExtensionValidation -Dskip.gradle.tests -Dtruststore.skip -Dno-test-modules -Dasciidoctor.fail-if=DEBUG clean install" +alias format="mvnd process-sources -Denforcer.skip -Dprotoc.skip" +alias qss="java -jar ${HOME}/git/quarkus/devtools/cli/target/quarkus-cli-999-SNAPSHOT-runner.jar" +``` + +- If using plain Maven + +```sh +alias build-fast="mvn -T0.8C -e -DskipDocs -DskipTests -DskipITs -Dinvoker.skip -DskipExtensionValidation -Dskip.gradle.tests -Dtruststore.skip clean install" +alias build-docs="mvn -T0.8C -e -DskipTests -DskipITs -Dinvoker.skip -DskipExtensionValidation -Dskip.gradle.tests -Dtruststore.skip -Dno-test-modules -Dasciidoctor.fail-if=DEBUG clean install" +alias format="mvn -T0.8C process-sources -Denforcer.skip -Dprotoc.skip" +alias qss="java -jar ${HOME}/git/quarkus/devtools/cli/target/quarkus-cli-999-SNAPSHOT-runner.jar" +``` + +Using `./mvnw` is often not practical in this case as you might want to call these aliases from a nested directory. +[gum](https://andresalmiray.com/gum-the-gradle-maven-wrapper/) might be useful in this case. + #### Building all modules of an extension Let's say you want to make changes to the `Jackson` extension. This extension contains the `deployment`, `runtime` diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 153d34aace15b..5836c5a86a11f 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -28,7 +28,7 @@ 1.1.7 2.1.5.Final 3.1.3.Final - 6.2.10.Final + 6.2.11.Final 0.33.0 0.2.4 0.1.15 @@ -37,7 +37,7 @@ 2.8.0-alpha 1.27.0-alpha 5.3.3 - 1.13.6 + 1.13.7 2.2.2 0.22.0 22.2 diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java index 9e252dcb0fb33..50f315897cd65 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java @@ -305,6 +305,7 @@ private BuildResult runAugment(boolean firstRun, Set changedResources, .setTargetDir(quarkusBootstrap.getTargetDirectory()) .setDeploymentClassLoader(deploymentClassLoader) .setBuildSystemProperties(quarkusBootstrap.getBuildSystemProperties()) + .setRuntimeProperties(quarkusBootstrap.getRuntimeProperties()) .setEffectiveModel(curatedApplication.getApplicationModel()) .setDependencyInfoProvider(quarkusBootstrap.getDependencyInfoProvider()); if (quarkusBootstrap.getBaseName() != null) { diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java index 0532cddf38cf4..6d21c80b275b2 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java @@ -19,6 +19,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.function.Consumer; @@ -70,7 +71,6 @@ import io.quarkus.fs.util.ZipUtils; import io.quarkus.gradle.tooling.ToolingUtils; import io.quarkus.gradle.workspace.descriptors.DefaultProjectDescriptor; -import io.quarkus.gradle.workspace.descriptors.ProjectDescriptor; import io.quarkus.gradle.workspace.descriptors.ProjectDescriptor.TaskType; import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactDependency; @@ -194,10 +194,13 @@ private ResolvedDependencyBuilder getProjectArtifact() { .setBuildDir(getLayout().getBuildDirectory().getAsFile().get().toPath()) .setBuildFile(getProjectBuildFile().getAsFile().get().toPath()); - ProjectDescriptor projectDescriptor = getProjectDescriptor().get(); - initProjectModule(projectDescriptor, mainModule, ArtifactSources.MAIN, DEFAULT_CLASSIFIER); + DefaultProjectDescriptor projectDescriptor = getProjectDescriptor().get(); + + initProjectModule(projectDescriptor, mainModule, ArtifactSources.MAIN, DEFAULT_CLASSIFIER, + getProjectDescriptor().get().getSourceSetTasksRaw()); if (getLaunchMode().get().isDevOrTest()) { - initProjectModule(projectDescriptor, mainModule, ArtifactSources.TEST, "tests"); + initProjectModule(projectDescriptor, mainModule, ArtifactSources.TEST, "tests", + getProjectDescriptor().get().getSourceSetTasksRaw()); } final PathList.Builder paths = PathList.builder(); collectDestinationDirs(mainModule.getMainSources().getSourceDirs(), paths); @@ -206,8 +209,8 @@ private ResolvedDependencyBuilder getProjectArtifact() { return appArtifact.setWorkspaceModule(mainModule).setResolvedPaths(paths.build()); } - private static void initProjectModule(ProjectDescriptor projectDescriptor, WorkspaceModule.Mutable module, - String sourceSetName, String classifier) { + private static void initProjectModule(DefaultProjectDescriptor projectDescriptor, WorkspaceModule.Mutable module, + String sourceSetName, String classifier, Map> sourceSetTasksRaw) { List sourceDirs = new ArrayList<>(); List resources = new ArrayList<>(); Set tasks = projectDescriptor.getTasksForSourceSet(sourceSetName.isEmpty() @@ -223,6 +226,23 @@ private static void initProjectModule(ProjectDescriptor projectDescriptor, Works resources.add(new DefaultSourceDir(source, destDir, null)); } } + + // Issue https://github.com/quarkusio/quarkus/issues/44384 + // no java sources are detected for compileJava before grpc configuration + // so we need to verify if the destination source for the task exist and add it manually + boolean containsJavaCompile = sourceDirs.stream() + .anyMatch(sourceDir -> "compileJava".equals(sourceDir.getValue("compiler", String.class))); + if (!containsJavaCompile && sourceSetTasksRaw.get("compileJava") != null) { + + sourceSetTasksRaw.get("compileJava").forEach(s -> { + File output = new File(s); + if (output.exists() && Objects.requireNonNull(output.listFiles()).length > 0) { + sourceDirs.add( + new DefaultSourceDir(output.toPath(), output.toPath(), null, Map.of("compiler", "compileJava"))); + } + }); + + } module.addArtifactSources(new DefaultArtifactSources(classifier, sourceDirs, resources)); } diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/DefaultProjectDescriptor.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/DefaultProjectDescriptor.java index da56ac82b63db..5ef42d35a75ef 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/DefaultProjectDescriptor.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/DefaultProjectDescriptor.java @@ -18,14 +18,16 @@ public class DefaultProjectDescriptor implements Serializable, ProjectDescriptor private final Map tasks; private final Map> sourceSetTasks; + private final Map> sourceSetTasksRaw; public DefaultProjectDescriptor(File projectDir, File buildDir, File buildFile, Map tasks, - Map> sourceSetTasks) { + Map> sourceSetTasks, Map> sourceSetTasksRaw) { this.projectDir = projectDir; this.buildDir = buildDir; this.buildFile = buildFile; this.tasks = tasks; this.sourceSetTasks = sourceSetTasks; + this.sourceSetTasksRaw = sourceSetTasksRaw; } @Override @@ -47,6 +49,10 @@ public Map> getSourceSetTasks() { return sourceSetTasks; } + public Map> getSourceSetTasksRaw() { + return sourceSetTasksRaw; + } + public Map getTasks() { return tasks; } @@ -81,7 +87,8 @@ public DefaultProjectDescriptor withSourceSetView(Set acceptedSourceSets Map filteredTasks = tasks.entrySet().stream() .filter(e -> filteredSourceSets.values().stream().anyMatch(tasks -> tasks.contains(e.getKey()))) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, TreeMap::new)); - return new DefaultProjectDescriptor(projectDir, buildDir, buildFile, filteredTasks, filteredSourceSets); + return new DefaultProjectDescriptor(projectDir, buildDir, buildFile, filteredTasks, filteredSourceSets, + sourceSetTasksRaw); } @Override @@ -92,6 +99,7 @@ public String toString() { ",\nbuildFile=" + buildFile + ",\ntasks=" + tasks + ",\nsourceSetTasks=" + sourceSetTasks + + ",\nsourceSetTasksRaw=" + sourceSetTasksRaw + "\n}"; } } diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptorBuilder.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptorBuilder.java index 9eab36470cb12..4bfbb5a6b1bbf 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptorBuilder.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/workspace/descriptors/ProjectDescriptorBuilder.java @@ -32,11 +32,13 @@ public class ProjectDescriptorBuilder { private final File buildFile; private final Map tasks; private final Map> sourceSetTasks; + private final Map> sourceSetTasksRaw; private final Set collectOnlySourceSets; private ProjectDescriptorBuilder(Project project, Set collectOnlySourceSets) { this.tasks = new LinkedHashMap<>(); this.sourceSetTasks = new LinkedHashMap<>(); + this.sourceSetTasksRaw = new LinkedHashMap<>(); this.buildFile = project.getBuildFile(); this.projectDir = project.getLayout().getProjectDirectory().getAsFile(); this.buildDir = project.getLayout().getBuildDirectory().get().getAsFile(); @@ -59,7 +61,8 @@ public static Provider buildForApp(Project target) { builder.buildDir, builder.buildFile, builder.tasks, - builder.sourceSetTasks)); + builder.sourceSetTasks, + builder.sourceSetTasksRaw)); } public static Provider buildForDependency(Project target) { @@ -76,10 +79,14 @@ public static Provider buildForDependency(Project targ builder.buildDir, builder.buildFile, builder.tasks, - builder.sourceSetTasks)); + builder.sourceSetTasks, + builder.sourceSetTasksRaw)); } private void readConfigurationFor(AbstractCompile task) { + sourceSetTasksRaw.computeIfAbsent(task.getName(), s -> new HashSet<>()) + .add(task.getDestinationDirectory().getAsFile().get().getAbsolutePath()); + if (task.getEnabled() && !task.getSource().isEmpty()) { File destDir = task.getDestinationDirectory().getAsFile().get(); task.getSource().visit(fileVisitDetails -> { diff --git a/devtools/gradle/pom.xml b/devtools/gradle/pom.xml index 398af0ca03beb..96c5786642531 100644 --- a/devtools/gradle/pom.xml +++ b/devtools/gradle/pom.xml @@ -192,6 +192,17 @@ assemble + + quickly-docs-build + + + quicklyDocs + + + + assemble + + quick-build-ci diff --git a/docs/src/main/asciidoc/funqy-gcp-functions.adoc b/docs/src/main/asciidoc/funqy-gcp-functions.adoc index 02109969b2499..85982963ee058 100644 --- a/docs/src/main/asciidoc/funqy-gcp-functions.adoc +++ b/docs/src/main/asciidoc/funqy-gcp-functions.adoc @@ -147,7 +147,7 @@ You will have a single JAR inside the `target/deployment` repository that contai Then you will be able to use `gcloud` to deploy your function to Google Cloud. The `gcloud` command will be different depending on which event triggers your function. -NOTE: We will use the Java 17 runtime but you can switch to the Java 21 runtime by using `--runtime=java21` instead of `--runtime=java17` on the deploy commands. +NOTE: We will use the Java 21 runtime, but you can switch to the Java 17 runtime by using `--runtime=java17` instead of `--runtime=java21` on the deploy commands. [WARNING] ==== @@ -168,7 +168,7 @@ Use this command to deploy to Google Cloud Functions: ---- gcloud functions deploy quarkus-example-funky-pubsub \ --entry-point=io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction \ - --runtime=java17 --trigger-resource hello_topic --trigger-event google.pubsub.topic.publish \ + --runtime=java21 --trigger-resource hello_topic --trigger-event google.pubsub.topic.publish \ --source=target/deployment ---- @@ -205,7 +205,7 @@ Then, use this command to deploy to Google Cloud Functions: ---- gcloud functions deploy quarkus-example-funky-storage \ --entry-point=io.quarkus.funqy.gcp.functions.FunqyBackgroundFunction \ - --runtime=java17 --trigger-resource quarkus-hello --trigger-event google.storage.object.finalize \ + --runtime=java21 --trigger-resource quarkus-hello --trigger-event google.storage.object.finalize \ --source=target/deployment ---- @@ -246,7 +246,7 @@ Then, use this command to deploy to Google Cloud Functions: ---- gcloud functions deploy quarkus-example-cloud-event --gen2 \ --entry-point=io.quarkus.funqy.gcp.functions.FunqyCloudEventsFunction \ - --runtime=java17 --trigger-bucket=example-cloud-event --source=target/deployment + --runtime=java21 --trigger-bucket=example-cloud-event --source=target/deployment ---- [IMPORTANT] @@ -371,7 +371,7 @@ This will call your cloud events function with an event on the `"MY_FILE.txt` fi Quarkus provides built-in support for testing your Funqy Google Cloud functions via the `quarkus-test-google-cloud-functions` dependency. -To use it, you must add the following test dependency in your `pom.xml`. +To use it, you must add the following test dependencies in your `pom.xml`. [source,xml] ---- @@ -380,6 +380,11 @@ To use it, you must add the following test dependency in your `pom.xml`. quarkus-test-google-cloud-functions test + + io.rest-assured + rest-assured + test + ---- This extension provides a `@WithFunction` annotation that can be used to annotate `@QuarkusTest` test cases to start a Cloud Function invoker before you test cases and stop it at the end. diff --git a/docs/src/main/asciidoc/gcp-functions-http.adoc b/docs/src/main/asciidoc/gcp-functions-http.adoc index 39d72b68a2073..0d0040952902c 100644 --- a/docs/src/main/asciidoc/gcp-functions-http.adoc +++ b/docs/src/main/asciidoc/gcp-functions-http.adoc @@ -162,13 +162,13 @@ The result of the previous command is a single JAR file inside the `target/deplo Then you will be able to use `gcloud` to deploy your function to Google Cloud. -NOTE: We will use the Java 17 runtime but you can switch to the Java 21 runtime by using `--runtime=java21` instead of `--runtime=java17` on the deploy commands. +NOTE: We will use the Java 21 runtime, but you can switch to the Java 17 runtime by using `--runtime=java17` instead of `--runtime=java21` on the deploy commands. [source,bash] ---- gcloud functions deploy quarkus-example-http \ --entry-point=io.quarkus.gcp.functions.http.QuarkusHttpFunction \ - --runtime=java17 --trigger-http --allow-unauthenticated --source=target/deployment + --runtime=java21 --trigger-http --allow-unauthenticated --source=target/deployment ---- [IMPORTANT] diff --git a/docs/src/main/asciidoc/gcp-functions.adoc b/docs/src/main/asciidoc/gcp-functions.adoc index 319c73b226e9d..4c3a53bb2ddc0 100644 --- a/docs/src/main/asciidoc/gcp-functions.adoc +++ b/docs/src/main/asciidoc/gcp-functions.adoc @@ -242,7 +242,7 @@ The result of the previous command is a single JAR file inside the `target/deplo Then you will be able to use `gcloud` to deploy your function to Google Cloud. The `gcloud` command will be different depending on which event triggers your function. -NOTE: We will use the Java 17 runtime but you can switch to the Java 21 runtime by using `--runtime=java21` instead of `--runtime=java17` on the deploy commands. +NOTE: We will use the Java 21 runtime but you can switch to the Java 17 runtime by using `--runtime=java17` instead of `--runtime=java21` on the deploy commands. [WARNING] ==== @@ -262,7 +262,7 @@ This is an example command to deploy your `HttpFunction` to Google Cloud: ---- gcloud functions deploy quarkus-example-http \ --entry-point=io.quarkus.gcp.functions.QuarkusHttpFunction \ - --runtime=java17 --trigger-http --allow-unauthenticated --source=target/deployment + --runtime=java21 --trigger-http --allow-unauthenticated --source=target/deployment ---- [IMPORTANT] @@ -289,7 +289,7 @@ it needs to use `--trigger-event google.storage.object.finalize` and the `--trig gcloud functions deploy quarkus-example-storage \ --entry-point=io.quarkus.gcp.functions.QuarkusBackgroundFunction \ --trigger-resource quarkus-hello --trigger-event google.storage.object.finalize \ - --runtime=java17 --source=target/deployment + --runtime=java21 --source=target/deployment ---- [IMPORTANT] @@ -315,7 +315,7 @@ it needs to use `--trigger-event google.pubsub.topic.publish` and the `--trigger ---- gcloud functions deploy quarkus-example-pubsub \ --entry-point=io.quarkus.gcp.functions.QuarkusBackgroundFunction \ - --runtime=java17 --trigger-resource hello_topic --trigger-event google.pubsub.topic.publish --source=target/deployment + --runtime=java21 --trigger-resource hello_topic --trigger-event google.pubsub.topic.publish --source=target/deployment ---- [IMPORTANT] @@ -341,7 +341,7 @@ it needs to use `--trigger-bucket` parameter with the name of a previously creat ---- gcloud functions deploy quarkus-example-cloud-event --gen2 \ --entry-point=io.quarkus.gcp.functions.QuarkusCloudEventsFunction \ - --runtime=java17 --trigger-bucket=example-cloud-event --source=target/deployment + --runtime=java21 --trigger-bucket=example-cloud-event --source=target/deployment ---- [IMPORTANT] @@ -485,6 +485,11 @@ To use it, you must add the following test dependency in your `pom.xml`. quarkus-test-google-cloud-functions test + + io.rest-assured + rest-assured + test + ---- This extension provides a `@WithFunction` annotation that can be used to annotate `@QuarkusTest` test cases to start a Cloud Function invoker before you test cases and stop it at the end. diff --git a/docs/src/main/asciidoc/opentelemetry-tracing.adoc b/docs/src/main/asciidoc/opentelemetry-tracing.adoc index ef62e35cbb403..3b0196ca44697 100644 --- a/docs/src/main/asciidoc/opentelemetry-tracing.adoc +++ b/docs/src/main/asciidoc/opentelemetry-tracing.adoc @@ -121,7 +121,7 @@ We have 2 options: * Take a look at: xref:observability-devservices-lgtm.adoc[Getting Started with Grafana-OTel-LGTM]. -This features a Quarkus Dev service including a Grafana for visualizing data, Loki to store logs, Tempo to store traces and Prometheus to store metrics. Also provides and OTel collector to receive the data. +This features a Quarkus Dev service including a Grafana for visualizing data, Loki to store logs, Tempo to store traces and Prometheus to store metrics. Also provides an OTel collector to receive the data. === Jaeger to see traces option @@ -148,7 +148,7 @@ services: - "16686:16686" # Jaeger UI - "14268:14268" # Receive legacy OpenTracing traces, optional - "4317:4317" # OTLP gRPC receiver - - "4318:4318" # OTLP HTTP receiver, not yet used by Quarkus, optional + - "4318:4318" # OTLP HTTP receiver - "14250:14250" # Receive from external otel-collector, optional environment: - COLLECTOR_OTLP_ENABLED=true diff --git a/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonConfig.java b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonConfig.java index 426240ae842ed..e3ff5f25f4984 100644 --- a/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonConfig.java +++ b/extensions/container-image/container-image-docker-common/deployment/src/main/java/io/quarkus/container/image/docker/common/deployment/CommonConfig.java @@ -6,7 +6,9 @@ import io.quarkus.runtime.annotations.ConfigDocDefault; import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigGroup; +@ConfigGroup public interface CommonConfig { /** * Path to the JVM Dockerfile. diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java index 5652b15cc1ff9..77bdc8c63d43b 100644 --- a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java @@ -140,7 +140,7 @@ public interface KeycloakDevServicesConfig { boolean createRealm(); /** - * Specifies whether to create the default client id `quarkus-app` with a secret `secret`and register them + * Specifies whether to create the default client id `quarkus-app` with a secret `secret` and register them * if the {@link #createRealm} property is set to true. * For OIDC extension configuration properties `quarkus.oidc.client.id` and `quarkus.oidc.credentials.secret` will * be configured. diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/graal/DisableLoggingFeature.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/graal/DisableLoggingFeature.java index ccd4be97371f3..b1ec1897f9ebe 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/graal/DisableLoggingFeature.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/graal/DisableLoggingFeature.java @@ -16,7 +16,8 @@ public class DisableLoggingFeature implements Feature { "org.hibernate.Version", "org.hibernate.annotations.common.Version", "SQL dialect", - "org.hibernate.cfg.Environment" + "org.hibernate.cfg.Environment", + "org.hibernate.orm.connections.pooling" }; private final Map categoryMap = new HashMap<>(CATEGORIES.length); diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java index 6719ea8e1d128..1f813fb8d7c5c 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionManagerConfiguration.java @@ -72,47 +72,47 @@ public final class TransactionManagerConfiguration { */ @ConfigItem public ObjectStoreConfig objectStore; -} - -@ConfigGroup -class ObjectStoreConfig { - /** - * The name of the directory where the transaction logs will be stored when using the {@code file-system} object store. - * If the value is not absolute then the directory is relative - * to the user.dir system property. - */ - @ConfigItem(defaultValue = "ObjectStore") - public String directory; - - /** - * The type of object store. - */ - @ConfigItem(defaultValue = "file-system") - public ObjectStoreType type; - - /** - * The name of the datasource where the transaction logs will be stored when using the {@code jdbc} object store. - *

- * If undefined, it will use the default datasource. - */ - @ConfigItem - public Optional datasource = Optional.empty(); - /** - * Whether to create the table if it does not exist. - */ - @ConfigItem(defaultValue = "false") - public boolean createTable; - - /** - * Whether to drop the table on startup. - */ - @ConfigItem(defaultValue = "false") - public boolean dropTable; - - /** - * The prefix to apply to the table. - */ - @ConfigItem(defaultValue = "quarkus_") - public String tablePrefix; + @ConfigGroup + public static class ObjectStoreConfig { + /** + * The name of the directory where the transaction logs will be stored when using the {@code file-system} object store. + * If the value is not absolute then the directory is relative + * to the user.dir system property. + */ + @ConfigItem(defaultValue = "ObjectStore") + public String directory; + + /** + * The type of object store. + */ + @ConfigItem(defaultValue = "file-system") + public ObjectStoreType type; + + /** + * The name of the datasource where the transaction logs will be stored when using the {@code jdbc} object store. + *

+ * If undefined, it will use the default datasource. + */ + @ConfigItem + public Optional datasource = Optional.empty(); + + /** + * Whether to create the table if it does not exist. + */ + @ConfigItem(defaultValue = "false") + public boolean createTable; + + /** + * Whether to drop the table on startup. + */ + @ConfigItem(defaultValue = "false") + public boolean dropTable; + + /** + * The prefix to apply to the table. + */ + @ConfigItem(defaultValue = "quarkus_") + public String tablePrefix; + } } diff --git a/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java b/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java index 37a79003cd594..ce7cd265223b3 100644 --- a/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java +++ b/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java @@ -1,5 +1,6 @@ package io.quarkus.netty.runtime.graal; +import static io.netty.handler.codec.http.HttpHeaderValues.BR; import static io.netty.handler.codec.http.HttpHeaderValues.DEFLATE; import static io.netty.handler.codec.http.HttpHeaderValues.GZIP; import static io.netty.handler.codec.http.HttpHeaderValues.X_DEFLATE; @@ -43,13 +44,13 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.DefaultChannelPromise; import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.compression.Brotli; +import io.netty.handler.codec.compression.BrotliDecoder; import io.netty.handler.codec.compression.ZlibCodecFactory; import io.netty.handler.codec.compression.ZlibWrapper; -import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http2.Http2Exception; import io.netty.handler.ssl.ApplicationProtocolConfig; import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; @@ -518,6 +519,10 @@ protected EmbeddedChannel newContentDecoder(String contentEncoding) throws Excep return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(), ctx.channel().config(), ZlibCodecFactory.newZlibDecoder(wrapper)); } + if (Brotli.isAvailable() && BR.contentEqualsIgnoreCase(contentEncoding)) { + return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(), + ctx.channel().config(), new BrotliDecoder()); + } // 'identity' or unsupported return null; @@ -533,21 +538,23 @@ final class Target_io_netty_handler_codec_http2_DelegatingDecompressorFrameListe @Substitute protected EmbeddedChannel newContentDecompressor(ChannelHandlerContext ctx, CharSequence contentEncoding) throws Http2Exception { - if (!HttpHeaderValues.GZIP.contentEqualsIgnoreCase(contentEncoding) - && !HttpHeaderValues.X_GZIP.contentEqualsIgnoreCase(contentEncoding)) { - if (!HttpHeaderValues.DEFLATE.contentEqualsIgnoreCase(contentEncoding) - && !HttpHeaderValues.X_DEFLATE.contentEqualsIgnoreCase(contentEncoding)) { - return null; - } else { - ZlibWrapper wrapper = this.strict ? ZlibWrapper.ZLIB : ZlibWrapper.ZLIB_OR_NONE; - return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(), - ctx.channel().config(), - new ChannelHandler[] { ZlibCodecFactory.newZlibDecoder(wrapper) }); - } - } else { - return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(), ctx.channel().config(), - new ChannelHandler[] { ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP) }); + if (GZIP.contentEqualsIgnoreCase(contentEncoding) || X_GZIP.contentEqualsIgnoreCase(contentEncoding)) { + return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(), + ctx.channel().config(), ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)); } + if (DEFLATE.contentEqualsIgnoreCase(contentEncoding) || X_DEFLATE.contentEqualsIgnoreCase(contentEncoding)) { + final ZlibWrapper wrapper = strict ? ZlibWrapper.ZLIB : ZlibWrapper.ZLIB_OR_NONE; + // To be strict, 'deflate' means ZLIB, but some servers were not implemented correctly. + return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(), + ctx.channel().config(), ZlibCodecFactory.newZlibDecoder(wrapper)); + } + if (Brotli.isAvailable() && BR.contentEqualsIgnoreCase(contentEncoding)) { + return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(), + ctx.channel().config(), new BrotliDecoder()); + } + + // 'identity' or unsupported + return null; } } diff --git a/extensions/oidc-client-registration/runtime/banned-signatures.txt b/extensions/oidc-client-registration/runtime/banned-signatures.txt new file mode 100644 index 0000000000000..be3e7aa782379 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/banned-signatures.txt @@ -0,0 +1,2 @@ +@defaultMessage Don't jakarta.json.Json as it is ridiculously slow (see https://github.com/quarkusio/quarkus/issues/42748) +jakarta.json.Json diff --git a/extensions/oidc-client-registration/runtime/pom.xml b/extensions/oidc-client-registration/runtime/pom.xml index 5a7b0038d55bb..94a4d7a812aa2 100644 --- a/extensions/oidc-client-registration/runtime/pom.xml +++ b/extensions/oidc-client-registration/runtime/pom.xml @@ -64,6 +64,20 @@ + + de.thetaphi + forbiddenapis + + + verify-forbidden-apis + + + ./banned-signatures.txt + + + + + diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java index 832460f2c0b1b..e1e24a19f0041 100644 --- a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java @@ -1,9 +1,10 @@ package io.quarkus.oidc.client.registration; +import static io.quarkus.jsonp.JsonProviderHolder.jsonProvider; + import java.util.List; import java.util.Map; -import jakarta.json.Json; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; @@ -66,11 +67,11 @@ public static class Builder { boolean built = false; Builder() { - builder = Json.createObjectBuilder(); + builder = jsonProvider().createObjectBuilder(); } Builder(JsonObject json) { - builder = Json.createObjectBuilder(json); + builder = jsonProvider().createObjectBuilder(json); } public Builder clientName(String clientName) { @@ -86,7 +87,7 @@ public Builder redirectUri(String redirectUri) { throw new IllegalStateException(); } builder.add(OidcConstants.CLIENT_METADATA_REDIRECT_URIS, - Json.createArrayBuilder().add(redirectUri).build()); + jsonProvider().createArrayBuilder().add(redirectUri).build()); return this; } @@ -95,7 +96,7 @@ public Builder postLogoutUri(String postLogoutUri) { throw new IllegalStateException(); } builder.add(OidcConstants.CLIENT_METADATA_POST_LOGOUT_URIS, - Json.createArrayBuilder().add(postLogoutUri).build()); + jsonProvider().createArrayBuilder().add(postLogoutUri).build()); return this; } @@ -103,7 +104,7 @@ public Builder extraProps(Map extraProps) { if (built) { throw new IllegalStateException(); } - builder.addAll(Json.createObjectBuilder(extraProps)); + builder.addAll(jsonProvider().createObjectBuilder(extraProps)); return this; } diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java index be4e72d3e87f3..15048803b4fb6 100644 --- a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java @@ -1,5 +1,7 @@ package io.quarkus.oidc.client.registration.runtime; +import static io.quarkus.jsonp.JsonProviderHolder.jsonProvider; + import java.io.IOException; import java.net.ConnectException; import java.util.List; @@ -7,7 +9,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; -import jakarta.json.Json; import jakarta.json.JsonObjectBuilder; import org.jboss.logging.Logger; @@ -250,17 +251,17 @@ static class ClientRegistrationHelper { } static ClientMetadata createMetadata(Metadata metadata) { - JsonObjectBuilder json = Json.createObjectBuilder(); + JsonObjectBuilder json = jsonProvider().createObjectBuilder(); if (metadata.clientName.isPresent()) { json.add(OidcConstants.CLIENT_METADATA_CLIENT_NAME, metadata.clientName.get()); } if (metadata.redirectUri.isPresent()) { json.add(OidcConstants.CLIENT_METADATA_REDIRECT_URIS, - Json.createArrayBuilder().add(metadata.redirectUri.get())); + jsonProvider().createArrayBuilder().add(metadata.redirectUri.get())); } if (metadata.postLogoutUri.isPresent()) { json.add(OidcConstants.POST_LOGOUT_REDIRECT_URI, - Json.createArrayBuilder().add(metadata.postLogoutUri.get())); + jsonProvider().createArrayBuilder().add(metadata.postLogoutUri.get())); } for (Map.Entry entry : metadata.extraProps.entrySet()) { json.add(entry.getKey(), entry.getValue()); diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java index 0288735390dc7..9c25b8c12cce3 100644 --- a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java @@ -1,12 +1,13 @@ package io.quarkus.oidc.client.registration.runtime; +import static io.quarkus.jsonp.JsonProviderHolder.jsonProvider; + import java.io.IOException; import java.net.ConnectException; import java.util.List; import java.util.Map; import java.util.Set; -import jakarta.json.Json; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.json.JsonValue; @@ -94,7 +95,7 @@ public Uni update(ClientMetadata newMetadata) { throw new OidcClientRegistrationException("Client secret can not be modified"); } - JsonObjectBuilder builder = Json.createObjectBuilder(); + JsonObjectBuilder builder = jsonProvider().createObjectBuilder(); JsonObject newJsonObject = newMetadata.getJsonObject(); JsonObject currentJsonObject = registeredMetadata.getJsonObject(); diff --git a/extensions/oidc-common/runtime/banned-signatures.txt b/extensions/oidc-common/runtime/banned-signatures.txt new file mode 100644 index 0000000000000..be3e7aa782379 --- /dev/null +++ b/extensions/oidc-common/runtime/banned-signatures.txt @@ -0,0 +1,2 @@ +@defaultMessage Don't jakarta.json.Json as it is ridiculously slow (see https://github.com/quarkusio/quarkus/issues/42748) +jakarta.json.Json diff --git a/extensions/oidc-common/runtime/pom.xml b/extensions/oidc-common/runtime/pom.xml index 4a8e750382909..3600d112cbda2 100644 --- a/extensions/oidc-common/runtime/pom.xml +++ b/extensions/oidc-common/runtime/pom.xml @@ -78,6 +78,20 @@ + + de.thetaphi + forbiddenapis + + + verify-forbidden-apis + + + ./banned-signatures.txt + + + + + diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/AbstractJsonObject.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/AbstractJsonObject.java index 9959b2292f6ad..186f14253993d 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/AbstractJsonObject.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/AbstractJsonObject.java @@ -1,5 +1,7 @@ package io.quarkus.oidc.common.runtime; +import static io.quarkus.jsonp.JsonProviderHolder.jsonProvider; + import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; @@ -7,7 +9,6 @@ import java.util.Map; import java.util.Set; -import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonNumber; import jakarta.json.JsonObject; @@ -20,7 +21,7 @@ public abstract class AbstractJsonObject { private JsonObject json; protected AbstractJsonObject() { - json = Json.createObjectBuilder().build(); + json = jsonProvider().createObjectBuilder().build(); } protected AbstractJsonObject(String jsonString) { @@ -54,7 +55,7 @@ public JsonObject getObject(String name) { } public JsonObject getJsonObject() { - return Json.createObjectBuilder(json).build(); + return jsonProvider().createObjectBuilder(json).build(); } public Object get(String name) { @@ -91,7 +92,7 @@ protected List getListOfStrings(String prop) { } public static JsonObject toJsonObject(String json) { - try (JsonReader jsonReader = Json.createReader(new StringReader(json))) { + try (JsonReader jsonReader = jsonProvider().createReader(new StringReader(json))) { return jsonReader.readObject(); } } diff --git a/extensions/oidc/runtime/banned-signatures.txt b/extensions/oidc/runtime/banned-signatures.txt new file mode 100644 index 0000000000000..be3e7aa782379 --- /dev/null +++ b/extensions/oidc/runtime/banned-signatures.txt @@ -0,0 +1,2 @@ +@defaultMessage Don't jakarta.json.Json as it is ridiculously slow (see https://github.com/quarkusio/quarkus/issues/42748) +jakarta.json.Json diff --git a/extensions/oidc/runtime/pom.xml b/extensions/oidc/runtime/pom.xml index 25bb4a266db35..70a6279628b80 100644 --- a/extensions/oidc/runtime/pom.xml +++ b/extensions/oidc/runtime/pom.xml @@ -88,6 +88,20 @@ + + de.thetaphi + forbiddenapis + + + verify-forbidden-apis + + + ./banned-signatures.txt + + + + + diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/AzureAccessTokenCustomizer.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/AzureAccessTokenCustomizer.java index edad83046e7be..0aa8b8e15ea50 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/AzureAccessTokenCustomizer.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/AzureAccessTokenCustomizer.java @@ -1,11 +1,12 @@ package io.quarkus.oidc.runtime.providers; +import static io.quarkus.jsonp.JsonProviderHolder.jsonProvider; + import java.nio.charset.StandardCharsets; import java.util.Base64; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Named; -import jakarta.json.Json; import jakarta.json.JsonObject; import io.quarkus.oidc.OIDCException; @@ -24,7 +25,7 @@ public JsonObject customizeHeaders(JsonObject headers) { if (nonce != null) { byte[] nonceSha256 = OidcUtils.getSha256Digest(nonce.getBytes(StandardCharsets.UTF_8)); byte[] newNonceBytes = Base64.getUrlEncoder().withoutPadding().encode(nonceSha256); - return Json.createObjectBuilder(headers) + return jsonProvider().createObjectBuilder(headers) .add(OidcConstants.NONCE, new String(newNonceBytes, StandardCharsets.UTF_8)).build(); } return null; diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java index 56809719f7b0a..725999bbd0b9a 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java @@ -18,15 +18,17 @@ public final class MessageBundleMethodBuildItem extends MultiBuildItem { private final MethodInfo method; private final String template; private final boolean isDefaultBundle; + private final boolean hasGeneratedTemplate; MessageBundleMethodBuildItem(String bundleName, String key, String templateId, MethodInfo method, String template, - boolean isDefaultBundle) { + boolean isDefaultBundle, boolean hasGeneratedTemplate) { this.bundleName = bundleName; this.key = key; this.templateId = templateId; this.method = method; this.template = template; this.isDefaultBundle = isDefaultBundle; + this.hasGeneratedTemplate = hasGeneratedTemplate; } public String getBundleName() { @@ -54,6 +56,11 @@ public MethodInfo getMethod() { return method; } + /** + * + * @return {@code true} if there is a corresponding method declared on the message bundle interface + * @see #getMethod() + */ public boolean hasMethod() { return method != null; } @@ -79,6 +86,14 @@ public boolean isDefaultBundle() { return isDefaultBundle; } + /** + * + * @return {@code true} if the template was generated, e.g. a message bundle method for an enum + */ + public boolean hasGeneratedTemplate() { + return hasGeneratedTemplate; + } + /** * * @return the path diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 8b3af1267819e..03783fc656530 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -701,8 +701,22 @@ void generateExamplePropertiesFiles(List messageBu List messages = entry.getValue(); messages.sort(Comparator.comparing(MessageBundleMethodBuildItem::getKey)); Path exampleProperties = generatedExamplesDir.resolve(entry.getKey() + ".properties"); - Files.write(exampleProperties, - messages.stream().map(m -> m.getMethod().name() + "=" + m.getTemplate()).collect(Collectors.toList())); + List lines = new ArrayList<>(); + for (MessageBundleMethodBuildItem m : messages) { + if (m.hasMethod()) { + if (m.hasGeneratedTemplate()) { + // Skip messages with generated templates + continue; + } + // Keys are mapped to method names + lines.add(m.getMethod().name() + "=" + m.getTemplate()); + } else { + // No corresponding method declared - use the key instead + // For example, there is no method for generated enum constant message keys + lines.add(m.getKey() + "=" + m.getTemplate()); + } + } + Files.write(exampleProperties, lines); } } @@ -991,6 +1005,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d } keyMap.put(key, new SimpleMessageMethod(method)); + boolean generatedTemplate = false; String messageTemplate = messageTemplates.get(method.name()); if (messageTemplate == null) { messageTemplate = getMessageAnnotationValue(messageAnnotation); @@ -1042,6 +1057,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d } generatedMessageTemplate.append("{/when}"); messageTemplate = generatedMessageTemplate.toString(); + generatedTemplate = true; } } } @@ -1067,7 +1083,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d } MessageBundleMethodBuildItem messageBundleMethod = new MessageBundleMethodBuildItem(bundleName, key, templateId, - method, messageTemplate, defaultBundleInterface == null); + method, messageTemplate, defaultBundleInterface == null, generatedTemplate); messageTemplateMethods .produce(messageBundleMethod); @@ -1138,8 +1154,7 @@ private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, Strin } MessageBundleMethodBuildItem messageBundleMethod = new MessageBundleMethodBuildItem(bundleName, enumConstantKey, - templateId, null, messageTemplate, - defaultBundleInterface == null); + templateId, null, messageTemplate, defaultBundleInterface == null, true); messageTemplateMethods.produce(messageBundleMethod); MethodCreator enumConstantMethod = bundleCreator.getMethodCreator(enumConstantKey, diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java new file mode 100644 index 0000000000000..4baf3b0756616 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java @@ -0,0 +1,31 @@ +package io.quarkus.qute.deployment; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.qute.runtime.devmode.QuteErrorPageSetup; + +@BuildSteps(onlyIf = IsDevelopment.class) +public class QuteDevModeProcessor { + + @BuildStep + void collectGeneratedContents(List templatePaths, + BuildProducer errors) { + Map contents = new HashMap<>(); + for (TemplatePathBuildItem template : templatePaths) { + if (!template.isFileBased()) { + contents.put(template.getPath(), template.getContent()); + } + } + // Set the global that could be used at runtime when a qute error page is rendered + DevConsoleManager.setGlobal(QuteErrorPageSetup.GENERATED_CONTENTS, contents); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumExampleFileTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumExampleFileTest.java new file mode 100644 index 0000000000000..008a289fa6340 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumExampleFileTest.java @@ -0,0 +1,64 @@ +package io.quarkus.qute.deployment.i18n; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Properties; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class MessageBundleEnumExampleFileTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot(root -> root + .addClasses(Messages.class, MyEnum.class) + .addAsResource(new StringAsset(""" + myEnum_ON=On + myEnum_OFF=Off + myEnum_UNDEFINED=Undefined + """), + "messages/enu.properties")); + + @ProdBuildResults + ProdModeTestResults testResults; + + @Test + public void testExampleProperties() throws FileNotFoundException, IOException { + Path path = testResults.getBuildDir().resolve("qute-i18n-examples").resolve("enu.properties"); + assertTrue(path.toFile().canRead()); + Properties props = new Properties(); + props.load(new FileInputStream(path.toFile())); + assertEquals(3, props.size()); + assertTrue(props.containsKey("myEnum_ON")); + assertTrue(props.containsKey("myEnum_OFF")); + assertTrue(props.containsKey("myEnum_UNDEFINED")); + } + + @MessageBundle(value = "enu", locale = "en") + public interface Messages { + + // Replaced with: + // @Message("{#when myEnum}" + // + "{#is ON}{enu:myEnum_ON}" + // + "{#is OFF}{enu:myEnum_OFF}" + // + "{#is UNDEFINED}{enu:myEnum_UNDEFINED}" + // + "{/when}") + @Message + String myEnum(MyEnum myEnum); + + } + +} diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java index 93c5fbe6b1327..b8b8a43ae5955 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java @@ -27,8 +27,7 @@ * There is a convenient way to localize enums. *

* If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined then - * it - * receives a generated template: + * it receives a generated template: * *

  * {#when enumParamName}
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 b7ced362defac..916522c98443f 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
@@ -2,6 +2,7 @@
 
 import java.io.BufferedReader;
 import java.io.IOException;
+import java.io.StringReader;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.nio.file.Files;
@@ -12,6 +13,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.ListIterator;
+import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.stream.Collectors;
@@ -19,6 +21,7 @@
 import org.jboss.logging.Logger;
 
 import io.quarkus.dev.ErrorPageGenerators;
+import io.quarkus.dev.console.DevConsoleManager;
 import io.quarkus.dev.spi.HotReplacementContext;
 import io.quarkus.dev.spi.HotReplacementSetup;
 import io.quarkus.qute.Engine;
@@ -33,6 +36,8 @@ public class QuteErrorPageSetup implements HotReplacementSetup {
 
     private static final Logger LOG = Logger.getLogger(QuteErrorPageSetup.class);
 
+    public static final String GENERATED_CONTENTS = "io.quarkus.qute.generatedContents";
+
     private static final String TEMPLATE_EXCEPTION = "io.quarkus.qute.TemplateException";
     private static final String ORIGIN = "io.quarkus.qute.TemplateNode$Origin";
 
@@ -139,6 +144,10 @@ String getProblemInfo(int index, Throwable problem, Template problemTemplate, Es
             LOG.warn("Unable to read the template source: " + templateId, e);
         }
 
+        if (sourceLines.isEmpty()) {
+            return Arrays.stream(messageLines).collect(Collectors.joining("
")); + } + List realLines = new ArrayList<>(); boolean endLinesSkipped = false; if (sourceLines.size() > 15) { @@ -187,6 +196,14 @@ private BufferedReader getBufferedReader(String templateId) throws IOException { } } } + // Source file not available - try to search the generated contents + Map generatedContents = DevConsoleManager.getGlobal(GENERATED_CONTENTS); + if (generatedContents != null) { + String template = generatedContents.get(templateId); + if (template != null) { + return new BufferedReader(new StringReader(template)); + } + } throw new IllegalStateException("Template source not available"); } diff --git a/extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java b/extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java index 9d154c483eb96..c1fa069d6e2cc 100644 --- a/extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java +++ b/extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java @@ -27,7 +27,6 @@ import org.jboss.jandex.Indexer; import org.jboss.jandex.MethodInfo; import org.jboss.logging.Logger; -import org.jboss.resteasy.reactive.common.jaxrs.RuntimeDelegateImpl; import org.jboss.resteasy.reactive.common.model.InterceptorContainer; import org.jboss.resteasy.reactive.common.model.PreMatchInterceptorContainer; import org.jboss.resteasy.reactive.common.model.ResourceInterceptor; @@ -340,8 +339,7 @@ public void setupEndpoints(BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, @BuildStep void registerRuntimeDelegateImpl(BuildProducer serviceProviders) { - serviceProviders.produce(new ServiceProviderBuildItem(RuntimeDelegate.class.getName(), - RuntimeDelegateImpl.class.getName())); + serviceProviders.produce(ServiceProviderBuildItem.allProvidersFromClassPath(RuntimeDelegate.class.getName())); } /* diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java index fa1fdb002bf0a..ebc47ab056de6 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java @@ -239,18 +239,18 @@ private boolean deserializeObject(ClassInfo classInfo, ResultHandle objHandle, C ResultHandle nextField = loopCreator .invokeInterfaceMethod(ofMethod(Iterator.class, "next", Object.class), fieldsIterator); ResultHandle mapEntry = loopCreator.checkCast(nextField, Map.Entry.class); - ResultHandle fieldName = loopCreator - .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getKey", Object.class), mapEntry); ResultHandle fieldValue = loopCreator.checkCast(loopCreator .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getValue", Object.class), mapEntry), JsonNode.class); - loopCreator.ifTrue(loopCreator.invokeVirtualMethod(ofMethod(JsonNode.class, "isNull", boolean.class), fieldValue)) - .trueBranch().continueScope(loopCreator); + BytecodeCreator fieldReader = loopCreator + .ifTrue(loopCreator.invokeVirtualMethod(ofMethod(JsonNode.class, "isNull", boolean.class), fieldValue)) + .falseBranch(); + + ResultHandle fieldName = fieldReader + .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getKey", Object.class), mapEntry); + Switch.StringSwitch strSwitch = fieldReader.stringSwitch(fieldName); - Set deserializedFields = new HashSet<>(); - ResultHandle deserializationContext = deserialize.getMethodParam(1); - Switch.StringSwitch strSwitch = loopCreator.stringSwitch(fieldName); - return deserializeFields(classCreator, classInfo, deserializationContext, objHandle, fieldValue, deserializedFields, + return deserializeFields(classCreator, classInfo, deserialize.getMethodParam(1), objHandle, fieldValue, new HashSet<>(), strSwitch, parseTypeParameters(classInfo, classCreator)); } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MapWrapper.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MapWrapper.java new file mode 100644 index 0000000000000..6bc5bda55d642 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/MapWrapper.java @@ -0,0 +1,32 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import java.util.Map; + +public class MapWrapper { + + private String name; + private Map properties; + + public MapWrapper() { + } + + public MapWrapper(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index 855c43625c09e..861f01ce08a96 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -123,6 +123,13 @@ public StateRecord echoDog(StateRecord stateRecord) { return stateRecord; } + @POST + @Path("/null-map-echo") + @Consumes(MediaType.APPLICATION_JSON) + public MapWrapper echoNullMap(MapWrapper mapWrapper) { + return mapWrapper; + } + @EnableSecureSerialization @GET @Path("/abstract-cat") diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java index d2f22569f9a7a..a5fa4d498c923 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java @@ -36,7 +36,7 @@ public JavaArchive get() { AbstractPet.class, Dog.class, Cat.class, Veterinarian.class, AbstractNamedPet.class, AbstractUnsecuredPet.class, UnsecuredPet.class, SecuredPersonInterface.class, Frog.class, Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class, - NestedInterface.class, StateRecord.class) + NestedInterface.class, StateRecord.class, MapWrapper.class) .addAsResource(new StringAsset("admin-expression=admin\n" + "user-expression=user\n" + "birth-date-roles=alice,bob\n"), "application.properties"); @@ -733,4 +733,18 @@ public void testRecordEcho() { assertTrue(first >= 0); assertEquals(first, last); } + + @Test + public void testNullMapEcho() { + RestAssured + .with() + .body(new MapWrapper("test")) + .contentType("application/json; charset=utf-8") + .post("/simple/null-map-echo") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("test")) + .body("properties", Matchers.nullValue()); + } } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java index 65dec05aa59a4..10ea3d373ce91 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java @@ -25,7 +25,7 @@ public JavaArchive get() { AbstractPet.class, Dog.class, Cat.class, Veterinarian.class, AbstractNamedPet.class, AbstractUnsecuredPet.class, UnsecuredPet.class, SecuredPersonInterface.class, Frog.class, Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class, - NestedInterface.class, StateRecord.class) + NestedInterface.class, StateRecord.class, MapWrapper.class) .addAsResource(new StringAsset("admin-expression=admin\n" + "user-expression=user\n" + "birth-date-roles=alice,bob\n" + diff --git a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/DelayedExecutionInvoker.java b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/DelayedExecutionInvoker.java index 6e343ac35ab5e..4faa121b25794 100644 --- a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/DelayedExecutionInvoker.java +++ b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/DelayedExecutionInvoker.java @@ -53,12 +53,7 @@ public CompletionStage invoke(ScheduledExecution execution) throws Excepti executor.schedule(new Runnable() { @Override public void run() { - try { - delegate.invoke(execution); - ret.complete(null); - } catch (Exception e) { - ret.completeExceptionally(e); - } + invokeComplete(ret, execution); } }, delay, TimeUnit.MILLISECONDS); return ret; diff --git a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/DelegateInvoker.java b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/DelegateInvoker.java index a2245862d7fb0..5b25e3bebd99b 100644 --- a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/DelegateInvoker.java +++ b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/DelegateInvoker.java @@ -2,6 +2,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; import io.quarkus.scheduler.ScheduledExecution; @@ -30,4 +31,17 @@ protected CompletionStage invokeDelegate(ScheduledExecution execution) { return CompletableFuture.failedStage(e); } } + + protected void invokeComplete(CompletableFuture ret, ScheduledExecution execution) { + invokeDelegate(execution).whenComplete(new BiConsumer<>() { + @Override + public void accept(Void r, Throwable t) { + if (t != null) { + ret.completeExceptionally(t); + } else { + ret.complete(null); + } + } + }); + } } diff --git a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/OffloadingInvoker.java b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/OffloadingInvoker.java index 3026c382fd282..23b8605aa6e5a 100644 --- a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/OffloadingInvoker.java +++ b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/OffloadingInvoker.java @@ -1,6 +1,7 @@ package io.quarkus.scheduler.common.runtime; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import io.quarkus.scheduler.ScheduledExecution; @@ -28,6 +29,7 @@ public OffloadingInvoker(ScheduledInvoker delegate, Vertx vertx) { @Override public CompletionStage invoke(ScheduledExecution execution) throws Exception { + CompletableFuture ret = new CompletableFuture<>(); Context context = VertxContext.getOrCreateDuplicatedContext(vertx); VertxContextSafetyToggle.setContextSafe(context, true); if (delegate.isBlocking()) { @@ -40,7 +42,7 @@ public void handle(Void event) { VirtualThreadsRecorder.getCurrent().execute(new Runnable() { @Override public void run() { - doInvoke(execution); + invokeComplete(ret, execution); } }); } @@ -49,7 +51,7 @@ public void run() { context.executeBlocking(new Callable() { @Override public Void call() { - doInvoke(execution); + invokeComplete(ret, execution); return null; } }, false); @@ -58,19 +60,11 @@ public Void call() { context.runOnContext(new Handler() { @Override public void handle(Void event) { - doInvoke(execution); + invokeComplete(ret, execution); } }); } - return null; - } - - void doInvoke(ScheduledExecution execution) { - try { - delegate.invoke(execution); - } catch (Throwable t) { - // already logged by the StatusEmitterInvoker - } + return ret; } } diff --git a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/ScheduledInvoker.java b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/ScheduledInvoker.java index a7f1f6a80e702..b57f91648ddfe 100644 --- a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/ScheduledInvoker.java +++ b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/ScheduledInvoker.java @@ -11,7 +11,7 @@ public interface ScheduledInvoker { /** * @param execution - * @return the result + * @return the result, never {@code null} * @throws Exception */ CompletionStage invoke(ScheduledExecution execution) throws Exception; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java index 6d839c1277323..1a1355759037d 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java @@ -1100,7 +1100,7 @@ private void implementCreateForSyntheticBean(ClassCreator beanCreator, BeanInfo List matchingIPs = new ArrayList<>(); for (InjectionPointInfo injectionPoint : bean.getDeployment().getInjectionPoints()) { - if (bean.equals(injectionPoint.getResolvedBean())) { + if (!injectionPoint.isSynthetic() && bean.equals(injectionPoint.getResolvedBean())) { matchingIPs.add(injectionPoint); } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java index 1b747cb158390..b332eb3e72bff 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java @@ -81,8 +81,10 @@ public void addPlatformDescriptor(String groupId, String artifactId, String clas artifactId.substring(0, artifactId.length() - BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX.length()), version); - platformImports.computeIfAbsent(bomCoords, c -> new PlatformImport()).descriptorFound = true; - platformBoms.add(bomCoords); + platformImports.computeIfAbsent(bomCoords, c -> { + platformBoms.add(bomCoords); + return new PlatformImport(); + }).descriptorFound = true; } public void addPlatformProperties(String groupId, String artifactId, String classifier, String type, String version, @@ -92,21 +94,24 @@ public void addPlatformProperties(String groupId, String artifactId, String clas artifactId.length() - BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX.length()), version); platformImports.computeIfAbsent(bomCoords, c -> new PlatformImport()); - importedPlatformBoms.computeIfAbsent(groupId, g -> new ArrayList<>()).add(bomCoords); - - final Properties props = new Properties(); - try (InputStream is = Files.newInputStream(propsPath)) { - props.load(is); - } catch (IOException e) { - throw new AppModelResolverException("Failed to read properties from " + propsPath, e); - } - for (Map.Entry prop : props.entrySet()) { - final String name = String.valueOf(prop.getKey()); - if (name.startsWith(BootstrapConstants.PLATFORM_PROPERTY_PREFIX)) { - if (isPlatformReleaseInfo(name)) { - addPlatformRelease(name, String.valueOf(prop.getValue())); - } else { - collectedProps.putIfAbsent(name, String.valueOf(prop.getValue().toString())); + importedPlatformBoms.computeIfAbsent(groupId, g -> new ArrayList<>()); + if (!importedPlatformBoms.get(groupId).contains(bomCoords)) { + importedPlatformBoms.get(groupId).add(bomCoords); + + final Properties props = new Properties(); + try (InputStream is = Files.newInputStream(propsPath)) { + props.load(is); + } catch (IOException e) { + throw new AppModelResolverException("Failed to read properties from " + propsPath, e); + } + for (Map.Entry prop : props.entrySet()) { + final String name = String.valueOf(prop.getKey()); + if (name.startsWith(BootstrapConstants.PLATFORM_PROPERTY_PREFIX)) { + if (isPlatformReleaseInfo(name)) { + addPlatformRelease(name, String.valueOf(prop.getValue())); + } else { + collectedProps.putIfAbsent(name, String.valueOf(prop.getValue().toString())); + } } } } diff --git a/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformImportsTest.java b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformImportsTest.java index 56106751314b2..1a0e70abcbe7d 100644 --- a/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformImportsTest.java +++ b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformImportsTest.java @@ -114,6 +114,14 @@ public void multiplePlatformReleaseInTheSameStream() throws Exception { GACTV.fromString("io.playground:acme-bom::pom:2.2.2"))))); } + @Test + public void duplicatePlatformDescriptorsAreIgnored() { + final PlatformImportsImpl pi = new PlatformImportsImpl(); + pi.addPlatformDescriptor("io.playground", "acme-bom-quarkus-platform-descriptor", "", "", "1.1"); + pi.addPlatformDescriptor("io.playground", "acme-bom-quarkus-platform-descriptor", "", "", "1.1"); + assertEquals(1, pi.getImportedPlatformBoms().size()); + } + private PlatformProps newPlatformProps() throws IOException { final PlatformProps p = new PlatformProps(); platformProps.add(p); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java index 852e3c9147f89..2a9a7f0462fb5 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java @@ -356,9 +356,7 @@ public QuarkusClassLoader createDeploymentClassLoader() { if (configuredClassLoading.isRemovedArtifact(dependency.getKey())) { continue; } - if (dependency.isRuntimeCp() && dependency.isJar() && - (dependency.isReloadable() && appModel.getReloadableWorkspaceDependencies().contains(dependency.getKey()) || - configuredClassLoading.isReloadableArtifact(dependency.getKey()))) { + if (isReloadableRuntimeDependency(dependency)) { processCpElement(dependency, element -> addCpElement(builder, dependency, element)); } } @@ -368,6 +366,12 @@ public QuarkusClassLoader createDeploymentClassLoader() { return builder.build(); } + private boolean isReloadableRuntimeDependency(ResolvedDependency dependency) { + return dependency.isRuntimeCp() && dependency.isJar() && + (dependency.isReloadable() && appModel.getReloadableWorkspaceDependencies().contains(dependency.getKey()) || + configuredClassLoading.isReloadableArtifact(dependency.getKey())); + } + public String getClassLoaderNameSuffix() { return quarkusBootstrap.getBaseName() != null ? " for " + quarkusBootstrap.getBaseName() : ""; } @@ -405,9 +409,7 @@ public QuarkusClassLoader createRuntimeClassLoader(ClassLoader base, Map addCpElement(builder, dependency, element)); } } 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 9368790bf7509..7f8ff90de6705 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 @@ -625,22 +625,77 @@ public List getElementsWithResource(String name) { public List getElementsWithResource(String name, boolean localOnly) { ensureOpen(name); - boolean parentFirst = parentFirst(name, getClassPathResourceIndex()); + final boolean parentFirst = parentFirst(name, getClassPathResourceIndex()); - List ret = new ArrayList<>(); + List result = List.of(); - if (parentFirst && !localOnly && parent instanceof QuarkusClassLoader) { - ret.addAll(((QuarkusClassLoader) parent).getElementsWithResource(name)); + if (parentFirst && !localOnly && parent instanceof QuarkusClassLoader parentQcl) { + result = parentQcl.getElementsWithResource(name); } - List classPathElements = getClassPathResourceIndex().getClassPathElements(name); - ret.addAll(classPathElements); + result = joinAndDedupe(result, getClassPathResourceIndex().getClassPathElements(name)); - if (!parentFirst && !localOnly && parent instanceof QuarkusClassLoader) { - ret.addAll(((QuarkusClassLoader) parent).getElementsWithResource(name)); + if (!parentFirst && !localOnly && parent instanceof QuarkusClassLoader parentQcl) { + result = joinAndDedupe(result, parentQcl.getElementsWithResource(name)); } - return ret; + return result; + } + + /** + * Returns a list containing elements from two lists eliminating duplicates. Elements from the first list + * will appear in the result before elements from the second list. + *

+ * The current implementation assumes that none of the lists contains duplicates on their own but some elements + * may be present in both lists. + * + * @param list1 first list + * @param list2 second list + * @return resulting list + */ + private static List joinAndDedupe(List list1, List list2) { + // it appears, in the vast majority of cases at least one of the lists will be empty + if (list1.isEmpty()) { + return list2; + } + if (list2.isEmpty()) { + return list1; + } + final List result = new ArrayList<>(list1.size() + list2.size()); + // it looks like in most cases at this point list1 (representing elements from the parent cl) will contain only one element + if (list1.size() == 1) { + final T firstCpe = list1.get(0); + result.add(firstCpe); + for (var cpe : list2) { + if (cpe != firstCpe) { + result.add(cpe); + } + } + return result; + } + result.addAll(list1); + for (var cpe : list2) { + if (!containsReference(list1, cpe)) { + result.add(cpe); + } + } + return result; + } + + /** + * Checks whether a list contains an element that references the other argument. + * + * @param list list of elements + * @param e element to look for + * @return true if the list contains an element referencing {@code e}, otherwise - false + */ + private static boolean containsReference(List list, T e) { + for (int i = list.size() - 1; i >= 0; --i) { + if (e == list.get(i)) { + return true; + } + } + return false; } public Set getReloadableClassNames() { @@ -902,6 +957,10 @@ public QuarkusClassLoader build() { return new QuarkusClassLoader(this); } + @Override + public String toString() { + return "QuarkusClassLoader.Builder:" + name + "@" + Integer.toHexString(hashCode()); + } } public ClassLoader parent() { diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/jbang/JBangDevModeLauncherImpl.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/jbang/JBangDevModeLauncherImpl.java index 207281a09cb35..49dbca6aee629 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/jbang/JBangDevModeLauncherImpl.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/jbang/JBangDevModeLauncherImpl.java @@ -15,6 +15,7 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Map; +import java.util.Properties; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -85,7 +86,7 @@ public static Closeable main(String... args) { Path srcDir = projectRoot.resolve("src/main/java"); Files.createDirectories(srcDir); - Files.createSymbolicLink(srcDir.resolve(sourceFile.getFileName().toString()), sourceFile); + Path source = Files.createSymbolicLink(srcDir.resolve(sourceFile.getFileName().toString()), sourceFile); final LocalProject currentProject = LocalProject.loadWorkspace(projectRoot); final ResolvedDependency appArtifact = ResolvedDependencyBuilder.newInstance() .setCoords(currentProject.getAppArtifact(ArtifactCoords.TYPE_JAR)) @@ -93,6 +94,8 @@ public static Closeable main(String... args) { .setWorkspaceModule(currentProject.toWorkspaceModule()) .build(); + Properties configurationProperties = getConfigurationProperties(source); + //todo : proper support for everything final QuarkusBootstrap.Builder builder = QuarkusBootstrap.builder() .setBaseClassLoader(JBangDevModeLauncherImpl.class.getClassLoader()) @@ -117,7 +120,9 @@ public static Closeable main(String... args) { return artifact; }).collect(Collectors.toList())) .setApplicationRoot(targetClasses) - .setProjectRoot(projectRoot); + .setProjectRoot(projectRoot) + .setBuildSystemProperties(configurationProperties) + .setRuntimeProperties(configurationProperties); Map context = new HashMap<>(); context.put("app-project", currentProject); @@ -174,4 +179,19 @@ private static String getQuarkusVersion() { throw new RuntimeException(e); } } + + private static Properties getConfigurationProperties(final Path source) throws IOException { + Properties properties = new Properties(); + for (String line : Files.readAllLines(source)) { + if (line.startsWith("//Q:CONFIG")) { + String conf = line.substring(10).trim(); + int equals = conf.indexOf("="); + if (equals == -1) { + throw new RuntimeException("invalid config " + line); + } + properties.setProperty(conf.substring(0, equals), conf.substring(equals + 1)); + } + } + return properties; + } } diff --git a/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/build.gradle.kts b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/build.gradle.kts new file mode 100644 index 0000000000000..8a97977aab3f8 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("io.quarkus") +} + +dependencies { + implementation(project(":module1")) + + implementation("io.quarkus:quarkus-rest-jackson") + implementation("io.quarkus:quarkus-rest") + implementation("io.quarkus:quarkus-kotlin") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-grpc") + + testImplementation("io.quarkus:quarkus-junit5") + testImplementation("io.rest-assured:rest-assured") +} diff --git a/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/src/main/kotlin/org/acme/GreetingResource.kt b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/src/main/kotlin/org/acme/GreetingResource.kt new file mode 100644 index 0000000000000..3fe47cc77e17a --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/src/main/kotlin/org/acme/GreetingResource.kt @@ -0,0 +1,23 @@ +package org.acme + +import io.quarkus.grpc.GrpcClient +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.Produces +import jakarta.ws.rs.core.MediaType +import org.acme.module1.SomeClass1 +import org.acme.proto.Greeter + +@Path("/version") +class GreetingResource { + + @GrpcClient + lateinit var hello: Greeter + + @GET + @Produces(MediaType.TEXT_PLAIN) + fun getVersion(): String { + return SomeClass1().getVersion() + } +} + diff --git a/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/src/main/resources/META-INF/resources/index.html b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000000000..c2ccecac788cf --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,285 @@ + + + + + code-with-quarkus - 1.0.0-SNAPSHOT + + + +

+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.properties

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/kotlin

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive Jackson
  • +
  • RESTEasy Reactive (guide)
  • +
  • Kotlin (guide)
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/src/main/resources/application.properties new file mode 100644 index 0000000000000..dd47fa0de94ee --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/application/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.generate-code.grpc.scan-for-proto=org.acme:module1 \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/build.gradle.kts b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/build.gradle.kts new file mode 100644 index 0000000000000..8a0bba94937f6 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + kotlin("jvm") version "2.0.21" + kotlin("plugin.allopen") version "2.0.21" + + id("io.quarkus") apply false +} +allprojects { + + repositories { + mavenLocal { + content { + includeGroupByRegex("io.quarkus.*") + includeGroup("org.hibernate.orm") + } + } + mavenCentral() + } + +} +subprojects { + apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "org.jetbrains.kotlin.plugin.allopen") + + + tasks.withType { + systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager") + } + + val quarkusPlatformGroupId: String by project + val quarkusPlatformArtifactId: String by project + val quarkusPlatformVersion: String by project + + dependencies { + implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + } + + allOpen { + annotation("jakarta.ws.rs.Path") + annotation("jakarta.enterprise.context.ApplicationScoped") + annotation("jakarta.persistence.Entity") + annotation("io.quarkus.test.junit.QuarkusTest") + } +} diff --git a/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/gradle.properties b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/gradle.properties new file mode 100644 index 0000000000000..c1ec5e0323249 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/gradle.properties @@ -0,0 +1,5 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus + +group=org.acme +version=1.0.0-SNAPSHOT diff --git a/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/module1/build.gradle.kts b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/module1/build.gradle.kts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/module1/src/main/kotlin/org/acme/module1/SomeClass1.kt b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/module1/src/main/kotlin/org/acme/module1/SomeClass1.kt new file mode 100644 index 0000000000000..5bb8560fce908 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/module1/src/main/kotlin/org/acme/module1/SomeClass1.kt @@ -0,0 +1,7 @@ +package org.acme.module1 + +class SomeClass1 { + fun getVersion(): String { + return "123" + } +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/module1/src/main/resources/proto/module1.proto b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/module1/src/main/resources/proto/module1.proto new file mode 100644 index 0000000000000..0378c5b7844a8 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/module1/src/main/resources/proto/module1.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "org.acme.proto"; +option java_outer_classname = "HelloWorldProto"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/settings.gradle b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/settings.gradle new file mode 100644 index 0000000000000..7b5aee200c8e0 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/grpc-multi-module-no-java/settings.gradle @@ -0,0 +1,22 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() + } + //noinspection GroovyAssignabilityCheck + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} + +rootProject.name = 'quarkus-grpc-multi-module-no-java' + +include ':module1' +include ':application' + diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcMultiModuleNoJavaQuarkusBuildTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcMultiModuleNoJavaQuarkusBuildTest.java new file mode 100644 index 0000000000000..e31a2b8e1b78d --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/GrpcMultiModuleNoJavaQuarkusBuildTest.java @@ -0,0 +1,27 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +public class GrpcMultiModuleNoJavaQuarkusBuildTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testGrpcMultiModuleBuild() throws Exception { + + final File projectDir = getProjectDir("grpc-multi-module-no-java"); + + final BuildResult build = runGradleWrapper(projectDir, "clean", "build"); + assertThat(BuildResult.isSuccessful(build.getTasks().get(":application:quarkusBuild"))).isTrue(); + assertThat(BuildResult.isSuccessful(build.getTasks().get(":application:quarkusAppPartsBuild"))).isTrue(); + + final Path applicationLib = projectDir.toPath().resolve("application").resolve("build").resolve("quarkus-app") + .resolve("lib").resolve("main"); + assertThat(applicationLib).exists(); + assertThat(applicationLib.resolve("org.acme.module1-1.0.0-SNAPSHOT.jar")).exists(); + } + +} diff --git a/integration-tests/vertx-http-compressors/all/src/main/java/io/quarkus/compressors/it/AllDecompressResource.java b/integration-tests/vertx-http-compressors/all/src/main/java/io/quarkus/compressors/it/AllDecompressResource.java new file mode 100644 index 0000000000000..43f8794d8f3cd --- /dev/null +++ b/integration-tests/vertx-http-compressors/all/src/main/java/io/quarkus/compressors/it/AllDecompressResource.java @@ -0,0 +1,7 @@ +package io.quarkus.compressors.it; + +import jakarta.ws.rs.Path; + +@Path("/decompressed") +public class AllDecompressResource extends DecompressResource { +} diff --git a/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java b/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java index 1786ff5b8e7f2..e2a59400de177 100644 --- a/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java +++ b/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java @@ -1,6 +1,7 @@ package io.quarkus.compressors.it; -import static io.quarkus.compressors.it.Testflow.runTest; +import static io.quarkus.compressors.it.Testflow.runCompressorsTest; +import static io.quarkus.compressors.it.Testflow.runDecompressorsTest; import java.net.URL; @@ -14,7 +15,10 @@ public class RESTEndpointsTest { @TestHTTPResource(value = "/compressed") - URL url; + URL urlCompressed; + + @TestHTTPResource(value = "/decompressed") + URL urlDEcompressed; @ParameterizedTest @CsvSource(value = { @@ -31,6 +35,88 @@ public class RESTEndpointsTest { //@formatter:on }, delimiter = '|', ignoreLeadingAndTrailingWhitespace = true, nullValues = "null") public void testCompressors(String endpoint, String acceptEncoding, String contentEncoding, String contentLength) { - runTest(url.toString() + endpoint, acceptEncoding, contentEncoding, contentLength); + runCompressorsTest(urlCompressed.toString() + endpoint, acceptEncoding, contentEncoding, contentLength); + } + + @ParameterizedTest + @CsvSource(value = { + //@formatter:off + // Context | Accept-Encoding | Content-Encoding | Method + "/text | identity | br | POST", + "/text | identity | gzip | POST", + "/text | identity | deflate | POST", + "/text | identity | br | PUT", + "/text | identity | gzip | PUT", + "/text | identity | deflate | PUT", + "/text | deflate | br | POST", + "/text | deflate | gzip | POST", + "/text | deflate | deflate | POST", + "/text | gzip | br | PUT", + "/text | gzip | gzip | PUT", + "/text | gzip | deflate | PUT", + "/text | br | br | POST", + "/text | br | gzip | POST", + "/text | br | deflate | POST", + "/text | br | br | PUT", + "/text | br | gzip | PUT", + "/text | gzip,br,deflate | deflate | PUT", + "/json | identity | br | POST", + "/json | identity | gzip | POST", + "/json | identity | deflate | POST", + "/json | identity | br | PUT", + "/json | identity | gzip | PUT", + "/json | identity | deflate | PUT", + "/json | deflate | br | POST", + "/json | deflate | gzip | POST", + "/json | deflate | deflate | POST", + "/json | gzip | br | PUT", + "/json | gzip | gzip | PUT", + "/json | gzip | deflate | PUT", + "/json | br | br | POST", + "/json | br | gzip | POST", + "/json | br | deflate | POST", + "/json | br | br | PUT", + "/json | br | gzip | PUT", + "/json | gzip,br,deflate | deflate | PUT", + "/xml | identity | br | POST", + "/xml | identity | gzip | POST", + "/xml | identity | deflate | POST", + "/xml | identity | br | PUT", + "/xml | identity | gzip | PUT", + "/xml | identity | deflate | PUT", + "/xml | deflate | br | POST", + "/xml | deflate | gzip | POST", + "/xml | deflate | deflate | POST", + "/xml | gzip | br | PUT", + "/xml | gzip | gzip | PUT", + "/xml | gzip | deflate | PUT", + "/xml | br | br | POST", + "/xml | br | gzip | POST", + "/xml | br | deflate | POST", + "/xml | br | br | PUT", + "/xml | br | gzip | PUT", + "/xml | gzip,br,deflate | deflate | PUT", + "/xhtml | identity | br | POST", + "/xhtml | identity | gzip | POST", + "/xhtml | identity | deflate | POST", + "/xhtml | identity | br | PUT", + "/xhtml | identity | gzip | PUT", + "/xhtml | identity | deflate | PUT", + "/xhtml | deflate | br | POST", + "/xhtml | deflate | gzip | POST", + "/xhtml | deflate | deflate | POST", + "/xhtml | gzip | br | PUT", + "/xhtml | gzip | gzip | PUT", + "/xhtml | gzip | deflate | PUT", + "/xhtml | br | br | POST", + "/xhtml | br | gzip | POST", + "/xhtml | br | deflate | POST", + "/xhtml | br | br | PUT", + "/xhtml | br | gzip | PUT", + "/xhtml | gzip,br,deflate | deflate | PUT" + //@formatter:on + }, delimiter = '|', ignoreLeadingAndTrailingWhitespace = true, nullValues = "null") + public void testDecompressors(String endpoint, String acceptEncoding, String contentEncoding, String method) { + runDecompressorsTest(urlDEcompressed.toString() + endpoint, acceptEncoding, contentEncoding, method); } } diff --git a/integration-tests/vertx-http-compressors/all/src/test/resources/application.properties b/integration-tests/vertx-http-compressors/all/src/test/resources/application.properties index d7bbc1ca381a0..9e910a8390a20 100644 --- a/integration-tests/vertx-http-compressors/all/src/test/resources/application.properties +++ b/integration-tests/vertx-http-compressors/all/src/test/resources/application.properties @@ -1,4 +1,7 @@ +# Enables sending clients compressed responses. quarkus.http.enable-compression=true +# Enables decompressing requests from clients. +quarkus.http.enable-decompression=true # Brotli is not present by default, so we add it all here: quarkus.http.compressors=deflate,gzip,br # This test the level actually makes impact. When left to default, diff --git a/integration-tests/vertx-http-compressors/app/src/main/java/io/quarkus/compressors/it/DecompressResource.java b/integration-tests/vertx-http-compressors/app/src/main/java/io/quarkus/compressors/it/DecompressResource.java new file mode 100644 index 0000000000000..6e1a19bd6f18f --- /dev/null +++ b/integration-tests/vertx-http-compressors/app/src/main/java/io/quarkus/compressors/it/DecompressResource.java @@ -0,0 +1,90 @@ +package io.quarkus.compressors.it; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * Resource with endpoints that consume compressed data + * in POST and PUT bodies from the client. + * Depending on the accept-encoding, the data is then + * compressed again and sent to the client + *
+ * e.g. Client sends a gzipped POST body and receives + * a brotli compressed response body. + *
+ * The endpoint looks like a dummy echo service, but + * there is compression and decompression going on behind + * the scenes in Vert.x. -> Netty. + *
+ * See: https://github.com/quarkusio/quarkus/pull/44348 + */ +public class DecompressResource { + + @POST + @Path("/text") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String textPost(String text) { + return text; + } + + @PUT + @Path("/text") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String textPut(String text) { + return text; + } + + @POST + @Path("/json") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public String jsonPost(String json) { + return json; + } + + @PUT + @Path("/json") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public String jsonPut(String json) { + return json; + } + + @POST + @Path("/xml") + @Produces(MediaType.TEXT_XML) + @Consumes(MediaType.TEXT_XML) + public String xmlPost(String xml) { + return xml; + } + + @PUT + @Path("/xml") + @Produces(MediaType.TEXT_XML) + @Consumes(MediaType.TEXT_XML) + public String xmlPut(String xml) { + return xml; + } + + @POST + @Path("/xhtml") + @Produces(MediaType.APPLICATION_XHTML_XML) + @Consumes(MediaType.APPLICATION_XHTML_XML) + public String xhtmlPost(String xhtml) { + return xhtml; + } + + @PUT + @Path("/xhtml") + @Produces(MediaType.APPLICATION_XHTML_XML) + @Consumes(MediaType.APPLICATION_XHTML_XML) + public String xhtmlPut(String xhtml) { + return xhtml; + } +} diff --git a/integration-tests/vertx-http-compressors/app/src/test/java/io/quarkus/compressors/it/Testflow.java b/integration-tests/vertx-http-compressors/app/src/test/java/io/quarkus/compressors/it/Testflow.java index 378486123faf9..3f1e72cf6eb71 100644 --- a/integration-tests/vertx-http-compressors/app/src/test/java/io/quarkus/compressors/it/Testflow.java +++ b/integration-tests/vertx-http-compressors/app/src/test/java/io/quarkus/compressors/it/Testflow.java @@ -9,9 +9,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPOutputStream; + +import com.aayushatharva.brotli4j.Brotli4jLoader; +import com.aayushatharva.brotli4j.encoder.BrotliOutputStream; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -33,16 +40,21 @@ public class Testflow { // Vert.x/Netty versions over time. public static final int COMPRESSION_TOLERANCE_PERCENT = 2; + static { + // Our test code does compression + Brotli4jLoader.ensureAvailability(); + } + /** - * This test logic is shared by both "all" module and "some" module. - * See their RESTEndpointsTest classes. + * This test logic is shared by both "all" module and "some" module. See their RESTEndpointsTest classes. * * @param endpoint * @param acceptEncoding * @param contentEncoding * @param contentLength */ - public static void runTest(String endpoint, String acceptEncoding, String contentEncoding, String contentLength) { + public static void runCompressorsTest(String endpoint, String acceptEncoding, String contentEncoding, + String contentLength) { LOG.infof("Endpoint %s; Accept-Encoding: %s; Content-Encoding: %s; Content-Length: %s", endpoint, acceptEncoding, contentEncoding, contentLength); // RestAssured @@ -97,31 +109,89 @@ public static void runTest(String endpoint, String acceptEncoding, String conten expectedLength + " plus " + COMPRESSION_TOLERANCE_PERCENT + "% tolerance, i.e. " + expectedLengthWithTolerance + "."); } + assertEquals(TEXT, decompress(actualEncoding, response.body().getBytes()), "Unexpected body text."); + } catch (InterruptedException | ExecutionException e) { + fail(e); + } + } - final String body; - if (actualEncoding != null && !"identity".equalsIgnoreCase(actualEncoding)) { - EmbeddedChannel channel = null; - if ("gzip".equalsIgnoreCase(actualEncoding)) { - channel = new EmbeddedChannel(newZlibDecoder(ZlibWrapper.GZIP)); - } else if ("deflate".equalsIgnoreCase(actualEncoding)) { - channel = new EmbeddedChannel(newZlibDecoder(ZlibWrapper.ZLIB)); - } else if ("br".equalsIgnoreCase(actualEncoding)) { - channel = new EmbeddedChannel(new BrotliDecoder()); - } else { - fail("Unexpected compression used by server: " + actualEncoding); - } - channel.writeInbound(Unpooled.copiedBuffer(response.body().getBytes())); - channel.finish(); - final ByteBuf decompressed = channel.readInbound(); - body = decompressed.readCharSequence(decompressed.readableBytes(), StandardCharsets.UTF_8).toString(); - } else { - body = response.body().toString(StandardCharsets.UTF_8); - } - - assertEquals(TEXT, body, - "Unexpected body text."); + public static void runDecompressorsTest(String endpoint, String acceptEncoding, String contentEncoding, + String method) { + LOG.infof("Endpoint %s; Accept-Encoding: %s; Content-Encoding: %s; Method: %s", + endpoint, acceptEncoding, contentEncoding, method); + final WebClient client = WebClient.create(Vertx.vertx(), new WebClientOptions() + .setLogActivity(true) + .setFollowRedirects(true) + .setDecompressionSupported(false)); + final CompletableFuture> future = new CompletableFuture<>(); + client.postAbs(endpoint) + .putHeader(HttpHeaders.CONTENT_ENCODING.toString(), contentEncoding) + .putHeader(HttpHeaders.ACCEPT.toString(), "*/*") + .putHeader(HttpHeaders.USER_AGENT.toString(), "Tester") + .sendBuffer(compress(contentEncoding, TEXT), ar -> { + if (ar.succeeded()) { + future.complete(ar.result()); + } else { + future.completeExceptionally(ar.cause()); + } + }); + try { + final HttpResponse response = future.get(); + final String actualEncoding = response.headers().get("content-encoding"); + final String body = decompress(actualEncoding, response.body().getBytes()); + assertEquals(OK.code(), response.statusCode(), "Http status must be OK."); + assertEquals(TEXT, body, "Unexpected body text."); } catch (InterruptedException | ExecutionException e) { fail(e); } } + + public static Buffer compress(String algorithm, String payload) { + final ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + if ("gzip".equalsIgnoreCase(algorithm)) { + try (GZIPOutputStream gzipStream = new GZIPOutputStream(byteStream)) { + gzipStream.write(payload.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("Gzip compression failed", e); + } + return Buffer.buffer(byteStream.toByteArray()); + } else if ("br".equalsIgnoreCase(algorithm)) { + try (BrotliOutputStream brotliStream = new BrotliOutputStream(byteStream)) { + brotliStream.write(payload.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("Brotli compression failed", e); + } + return Buffer.buffer(byteStream.toByteArray()); + } else if ("deflate".equalsIgnoreCase(algorithm)) { + try (DeflaterOutputStream deflateStream = new DeflaterOutputStream(byteStream)) { + deflateStream.write(payload.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("Deflate compression failed", e); + } + return Buffer.buffer(byteStream.toByteArray()); + } else { + throw new IllegalArgumentException("Unsupported encoding: " + algorithm); + } + } + + public static String decompress(String algorithm, byte[] payload) { + if (algorithm != null && !"identity".equalsIgnoreCase(algorithm)) { + final EmbeddedChannel channel; + if ("gzip".equalsIgnoreCase(algorithm)) { + channel = new EmbeddedChannel(newZlibDecoder(ZlibWrapper.GZIP)); + } else if ("deflate".equalsIgnoreCase(algorithm)) { + channel = new EmbeddedChannel(newZlibDecoder(ZlibWrapper.ZLIB)); + } else if ("br".equalsIgnoreCase(algorithm)) { + channel = new EmbeddedChannel(new BrotliDecoder()); + } else { + throw new RuntimeException("Unexpected compression used by server: " + algorithm); + } + channel.writeInbound(Unpooled.copiedBuffer(payload)); + channel.finish(); + final ByteBuf decompressed = channel.readInbound(); + return decompressed.readCharSequence(decompressed.readableBytes(), StandardCharsets.UTF_8).toString(); + } else { + return new String(payload, StandardCharsets.UTF_8); + } + } } diff --git a/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java b/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java index d2f9efc94e39c..9763e151fd69f 100644 --- a/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java +++ b/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java @@ -1,6 +1,6 @@ package io.quarkus.compressors.it; -import static io.quarkus.compressors.it.Testflow.runTest; +import static io.quarkus.compressors.it.Testflow.runCompressorsTest; import java.net.URL; @@ -30,6 +30,6 @@ public class RESTEndpointsTest { //@formatter:on }, delimiter = '|', ignoreLeadingAndTrailingWhitespace = true, nullValues = "null") public void testCompressors(String endpoint, String acceptEncoding, String contentEncoding, String contentLength) { - runTest(url.toString() + endpoint, acceptEncoding, contentEncoding, contentLength); + runCompressorsTest(url.toString() + endpoint, acceptEncoding, contentEncoding, contentLength); } } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java index 36500263f4638..a36ee0cc8ac64 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -21,6 +22,8 @@ import org.jboss.jandex.Index; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtensionContext; import io.quarkus.bootstrap.BootstrapConstants; @@ -41,7 +44,8 @@ import io.quarkus.test.common.RestorableSystemProperties; import io.quarkus.test.common.TestClassIndexer; -public class AbstractJvmQuarkusTestExtension extends AbstractQuarkusTestWithContextExtension { +public class AbstractJvmQuarkusTestExtension extends AbstractQuarkusTestWithContextExtension + implements ExecutionCondition { protected static final String TEST_LOCATION = "test-location"; protected static final String TEST_CLASS = "test-class"; @@ -267,6 +271,42 @@ private Class findTestProfileAnnotation(Class c return null; } + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + if (!context.getTestClass().isPresent()) { + return ConditionEvaluationResult.enabled("No test class specified"); + } + if (context.getTestInstance().isPresent()) { + return ConditionEvaluationResult.enabled("Quarkus Test Profile tags only affect classes"); + } + String tagsStr = System.getProperty("quarkus.test.profile.tags"); + if ((tagsStr == null) || tagsStr.isEmpty()) { + return ConditionEvaluationResult.enabled("No Quarkus Test Profile tags"); + } + Class testProfile = getQuarkusTestProfile(context); + if (testProfile == null) { + return ConditionEvaluationResult.disabled("Test '" + context.getRequiredTestClass() + + "' is not annotated with '@QuarkusTestProfile' but 'quarkus.profile.test.tags' was set"); + } + QuarkusTestProfile profileInstance; + try { + profileInstance = testProfile.getConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + Set testProfileTags = profileInstance.tags(); + String[] tags = tagsStr.split(","); + for (String tag : tags) { + String trimmedTag = tag.trim(); + if (testProfileTags.contains(trimmedTag)) { + return ConditionEvaluationResult.enabled("Tag '" + trimmedTag + "' is present on '" + testProfile + + "' which is used on test '" + context.getRequiredTestClass()); + } + } + return ConditionEvaluationResult.disabled("Test '" + context.getRequiredTestClass() + + "' disabled because 'quarkus.profile.test.tags' don't match the tags of '" + testProfile + "'"); + } + protected static class PrepareResult { protected final AugmentAction augmentAction; protected final QuarkusTestProfile profileInstance; diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java index f68b87436684b..71776250160ec 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java @@ -16,6 +16,8 @@ import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.ParameterContext; @@ -38,7 +40,7 @@ public class QuarkusMainTestExtension extends AbstractJvmQuarkusTestExtension implements InvocationInterceptor, BeforeEachCallback, AfterEachCallback, ParameterResolver, BeforeAllCallback, - AfterAllCallback { + AfterAllCallback, ExecutionCondition { PrepareResult prepareResult; @@ -321,4 +323,9 @@ public void afterAll(ExtensionContext context) throws Exception { public void beforeAll(ExtensionContext context) throws Exception { currentTestClassStack.push(context.getRequiredTestClass()); } + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + return super.evaluateExecutionCondition(context); + } } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index 8afb99cf10bb4..91da01372bb78 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -24,7 +24,6 @@ import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; -import java.util.Set; import java.util.concurrent.CompletionException; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; @@ -54,8 +53,6 @@ import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; -import org.junit.jupiter.api.extension.ConditionEvaluationResult; -import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.ParameterContext; @@ -105,7 +102,7 @@ public class QuarkusTestExtension extends AbstractJvmQuarkusTestExtension implements BeforeEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, AfterEachCallback, BeforeAllCallback, InvocationInterceptor, AfterAllCallback, - ParameterResolver, ExecutionCondition { + ParameterResolver { private static final Logger log = Logger.getLogger(QuarkusTestExtension.class); @@ -1136,42 +1133,6 @@ private boolean testMethodInvokerHandlesParamType(Object testMethodInvoker, Para } } - @Override - public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { - if (!context.getTestClass().isPresent()) { - return ConditionEvaluationResult.enabled("No test class specified"); - } - if (context.getTestInstance().isPresent()) { - return ConditionEvaluationResult.enabled("Quarkus Test Profile tags only affect classes"); - } - String tagsStr = System.getProperty("quarkus.test.profile.tags"); - if ((tagsStr == null) || tagsStr.isEmpty()) { - return ConditionEvaluationResult.enabled("No Quarkus Test Profile tags"); - } - Class testProfile = getQuarkusTestProfile(context); - if (testProfile == null) { - return ConditionEvaluationResult.disabled("Test '" + context.getRequiredTestClass() - + "' is not annotated with '@QuarkusTestProfile' but 'quarkus.profile.test.tags' was set"); - } - QuarkusTestProfile profileInstance; - try { - profileInstance = testProfile.getConstructor().newInstance(); - } catch (Exception e) { - throw new RuntimeException(e); - } - Set testProfileTags = profileInstance.tags(); - String[] tags = tagsStr.split(","); - for (String tag : tags) { - String trimmedTag = tag.trim(); - if (testProfileTags.contains(trimmedTag)) { - return ConditionEvaluationResult.enabled("Tag '" + trimmedTag + "' is present on '" + testProfile - + "' which is used on test '" + context.getRequiredTestClass()); - } - } - return ConditionEvaluationResult.disabled("Test '" + context.getRequiredTestClass() - + "' disabled because 'quarkus.profile.test.tags' don't match the tags of '" + testProfile + "'"); - } - public static class ExtensionState extends QuarkusTestExtensionState { public ExtensionState(Closeable testResourceManager, Closeable resource, Runnable clearCallbacks) {