From 39f7f47ce6b10032ed56a3619ae1c71050247fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Mon, 22 Jul 2024 14:48:56 +0200 Subject: [PATCH 01/17] Fix missing volume access modifier in Reactive MySQL client tests (cherry picked from commit 82f68d343631a488bc49f469659d020e40a79b43) --- extensions/reactive-mysql-client/deployment/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/reactive-mysql-client/deployment/pom.xml b/extensions/reactive-mysql-client/deployment/pom.xml index 828ed81edb4c9..f903fd22c4837 100644 --- a/extensions/reactive-mysql-client/deployment/pom.xml +++ b/extensions/reactive-mysql-client/deployment/pom.xml @@ -214,7 +214,7 @@ ${project.basedir}/custom-mariadbconfig:/etc/mysql/conf.d${volume.access.modifier} - ${project.basedir}/src/test/resources/setup.sql:/docker-entrypoint-initdb.d/setup.sql + ${project.basedir}/src/test/resources/setup.sql:/docker-entrypoint-initdb.d/setup.sql${volume.access.modifier} From 7feb5dabc536f625bd9916994cfadb0d03b832e0 Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Mon, 22 Jul 2024 09:53:01 +0200 Subject: [PATCH 02/17] Updates Infinispan 15.0.6.Final and Protostream 5.0.7.Final (cherry picked from commit 307e86bd32c253ca5d01fb22876f51ff167824fb) --- bom/application/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 06690151c6e74..9df65f525be75 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -131,8 +131,8 @@ 1.2.6 2.2 5.10.3 - 15.0.5.Final - 5.0.5.Final + 15.0.6.Final + 5.0.7.Final 3.1.5 4.1.111.Final 1.16.0 From a549dead8a9f2837c1ac3591080f9b7691fee100 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 22 Jul 2024 15:55:37 +0200 Subject: [PATCH 03/17] Make sure we append the current buffer before appending the title The title ended up being added first, which was definitely not what we wanted. Fixes #42036 (cherry picked from commit 6dc20b6084b3fa5a8c2741f5c139236f4ff26dbe) --- .../docs/generation/AssembleDownstreamDocumentation.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java b/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java index 2a88d1d2b106c..a12f247b1ecb7 100755 --- a/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java +++ b/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java @@ -355,6 +355,15 @@ private static void copyAsciidoc(Path sourceFile, Path targetFile, Set d lineNumber++; if (!documentTitleFound && line.startsWith("= ")) { + // anything in the buffer needs to be appended + // we don't need to rewrite it as before the title we can only have the preamble + // and we don't want to change anything in the preamble + // if at some point we want to adjust the preamble, make sure to do it in a separate method and not reuse rewriteContent + if (currentBuffer.length() > 0) { + rewrittenGuide.append(currentBuffer); + currentBuffer.setLength(0); + } + // this is the document title rewrittenGuide.append(line.replace(PROJECT_NAME_ATTRIBUTE, RED_HAT_BUILD_OF_QUARKUS) + "\n"); documentTitleFound = true; From db3ece9de3ccdea40d297adec7e92e7cc0399588 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Mon, 22 Jul 2024 17:05:58 -0300 Subject: [PATCH 04/17] Revert "Remove deprecated classes from app-model" This reverts commit 700f48da56e461f22ebca7a49379a27b8e3b621f. (cherry picked from commit 515c232354fa2902ce0a10c730f5e78f50f86cc8) --- .../deployment/ApplicationArchive.java | 7 + .../deployment/ApplicationArchiveImpl.java | 16 ++ .../gradle/tasks/ExtensionDescriptorTask.java | 14 +- .../gradle/tasks/ValidateExtensionTask.java | 44 +++--- .../quarkus/bootstrap/model/AppArtifact.java | 135 ++++++++++++++++ .../bootstrap/model/AppArtifactCoords.java | 149 ++++++++++++++++++ .../bootstrap/model/AppArtifactKey.java | 139 ++++++++++++++++ .../bootstrap/model/AppDependency.java | 141 +++++++++++++++++ .../model/AppArtifactCoordsTest.java | 54 +++++++ .../maven/ExtensionDescriptorMojo.java | 3 +- 10 files changed, 674 insertions(+), 28 deletions(-) create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifact.java create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifactCoords.java create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifactKey.java create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppDependency.java create mode 100644 independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/AppArtifactCoordsTest.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ApplicationArchive.java b/core/deployment/src/main/java/io/quarkus/deployment/ApplicationArchive.java index d7f5f4700d71a..14cdfaa81eb75 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/ApplicationArchive.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/ApplicationArchive.java @@ -6,6 +6,7 @@ import org.jboss.jandex.IndexView; +import io.quarkus.bootstrap.model.AppArtifactKey; import io.quarkus.bootstrap.model.PathsCollection; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.ResolvedDependency; @@ -71,6 +72,12 @@ public interface ApplicationArchive { */ PathCollection getResolvedPaths(); + /** + * @deprecated in favor of {@link #getKey()} + * @return the artifact key or null if not available + */ + AppArtifactKey getArtifactKey(); + /** * * @return the artifact key or null if not available diff --git a/core/deployment/src/main/java/io/quarkus/deployment/ApplicationArchiveImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/ApplicationArchiveImpl.java index a7818759ba9b5..252e59aeaf3db 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/ApplicationArchiveImpl.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/ApplicationArchiveImpl.java @@ -8,6 +8,7 @@ import org.jboss.jandex.IndexView; +import io.quarkus.bootstrap.model.AppArtifactKey; import io.quarkus.bootstrap.model.PathsCollection; import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.maven.dependency.ArtifactKey; @@ -61,6 +62,21 @@ public PathCollection getResolvedPaths() { return PathList.from(openTree.getOriginalTree().getRoots()); } + @Override + @Deprecated + /** + * @deprecated in favor of {@link #getKey()} + * @return archive key + */ + public AppArtifactKey getArtifactKey() { + if (resolvedDependency == null) { + return null; + } + ArtifactKey artifactKey = resolvedDependency.getKey(); + return new AppArtifactKey(artifactKey.getGroupId(), artifactKey.getArtifactId(), artifactKey.getClassifier(), + artifactKey.getType()); + } + @Override public ArtifactKey getKey() { return resolvedDependency != null ? resolvedDependency.getKey() : null; diff --git a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ExtensionDescriptorTask.java b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ExtensionDescriptorTask.java index d58c4ba9e54b8..938f1d83199f8 100644 --- a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ExtensionDescriptorTask.java +++ b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ExtensionDescriptorTask.java @@ -12,7 +12,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Properties; import java.util.Set; @@ -37,13 +36,14 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.model.AppArtifactCoords; +import io.quarkus.bootstrap.model.AppArtifactKey; import io.quarkus.bootstrap.model.ApplicationModelBuilder; import io.quarkus.devtools.project.extensions.ScmInfoProvider; import io.quarkus.extension.gradle.QuarkusExtensionConfiguration; import io.quarkus.extension.gradle.dsl.Capability; import io.quarkus.extension.gradle.dsl.RemovedResource; import io.quarkus.fs.util.ZipUtils; -import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.GACT; @@ -113,9 +113,9 @@ private void generateQuarkusExtensionProperties(Path metaInfDir) { if (conditionalDependencies != null && !conditionalDependencies.isEmpty()) { final StringBuilder buf = new StringBuilder(); int i = 0; - buf.append(ArtifactCoords.fromString(conditionalDependencies.get(i++))); + buf.append(AppArtifactCoords.fromString(conditionalDependencies.get(i++)).toString()); while (i < conditionalDependencies.size()) { - buf.append(' ').append(ArtifactCoords.fromString(conditionalDependencies.get(i++))); + buf.append(' ').append(AppArtifactCoords.fromString(conditionalDependencies.get(i++)).toString()); } props.setProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES, buf.toString()); } @@ -315,7 +315,7 @@ private void computeArtifactCoords(ObjectNode extObject) { } } if (artifactNode == null || groupId == null || artifactId == null || version == null) { - final ArtifactCoords coords = ArtifactCoords.of( + final AppArtifactCoords coords = new AppArtifactCoords( groupId == null ? projectInfo.get("group") : groupId, artifactId == null ? projectInfo.get("name") : artifactId, null, @@ -363,7 +363,7 @@ private void computeQuarkusExtensions(ObjectNode extObject) { ObjectNode metadataNode = getMetadataNode(extObject); Set extensions = new HashSet<>(); for (ResolvedArtifact resolvedArtifact : getClasspath().getResolvedConfiguration().getResolvedArtifacts()) { - if (Objects.equals(resolvedArtifact.getExtension(), "jar")) { + if (resolvedArtifact.getExtension().equals("jar")) { Path p = resolvedArtifact.getFile().toPath(); if (Files.isDirectory(p) && isExtension(p)) { extensions.add(resolvedArtifact); @@ -382,7 +382,7 @@ private void computeQuarkusExtensions(ObjectNode extObject) { for (ResolvedArtifact extension : extensions) { ModuleVersionIdentifier id = extension.getModuleVersion().getId(); extensionArray - .add(ArtifactKey.of(id.getGroup(), id.getName(), extension.getClassifier(), extension.getExtension()) + .add(new AppArtifactKey(id.getGroup(), id.getName(), extension.getClassifier(), extension.getExtension()) .toGacString()); } } diff --git a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ValidateExtensionTask.java b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ValidateExtensionTask.java index f293844ddfa3a..172a88e778d5c 100644 --- a/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ValidateExtensionTask.java +++ b/devtools/gradle/gradle-extension-plugin/src/main/java/io/quarkus/extension/gradle/tasks/ValidateExtensionTask.java @@ -15,12 +15,12 @@ import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; +import io.quarkus.bootstrap.model.AppArtifactKey; import io.quarkus.extension.gradle.QuarkusExtensionConfiguration; import io.quarkus.gradle.tooling.dependency.ArtifactExtensionDependency; import io.quarkus.gradle.tooling.dependency.DependencyUtils; import io.quarkus.gradle.tooling.dependency.ExtensionDependency; import io.quarkus.gradle.tooling.dependency.ProjectExtensionDependency; -import io.quarkus.maven.dependency.ArtifactKey; public class ValidateExtensionTask extends DefaultTask { @@ -59,12 +59,12 @@ public void setDeploymentModuleClasspath(Configuration deploymentModuleClasspath public void validateExtension() { Set runtimeArtifacts = getRuntimeModuleClasspath().getResolvedConfiguration().getResolvedArtifacts(); - List deploymentModuleKeys = collectRuntimeExtensionsDeploymentKeys(runtimeArtifacts); - List invalidRuntimeArtifacts = findExtensionInConfiguration(runtimeArtifacts, deploymentModuleKeys); + List deploymentModuleKeys = collectRuntimeExtensionsDeploymentKeys(runtimeArtifacts); + List invalidRuntimeArtifacts = findExtensionInConfiguration(runtimeArtifacts, deploymentModuleKeys); Set deploymentArtifacts = getDeploymentModuleClasspath().getResolvedConfiguration() .getResolvedArtifacts(); - List existingDeploymentModuleKeys = findExtensionInConfiguration(deploymentArtifacts, + List existingDeploymentModuleKeys = findExtensionInConfiguration(deploymentArtifacts, deploymentModuleKeys); deploymentModuleKeys.removeAll(existingDeploymentModuleKeys); @@ -81,17 +81,21 @@ public void validateExtension() { } } - private List collectRuntimeExtensionsDeploymentKeys(Set runtimeArtifacts) { - List runtimeExtensions = new ArrayList<>(); + private List collectRuntimeExtensionsDeploymentKeys(Set runtimeArtifacts) { + List runtimeExtensions = new ArrayList<>(); for (ResolvedArtifact resolvedArtifact : runtimeArtifacts) { ExtensionDependency extension = DependencyUtils.getExtensionInfoOrNull(getProject(), resolvedArtifact); if (extension != null) { - if (extension instanceof ProjectExtensionDependency ped) { + if (extension instanceof ProjectExtensionDependency) { + final ProjectExtensionDependency ped = (ProjectExtensionDependency) extension; + runtimeExtensions - .add(ArtifactKey.ga(ped.getDeploymentModule().getGroup().toString(), + .add(new AppArtifactKey(ped.getDeploymentModule().getGroup().toString(), ped.getDeploymentModule().getName())); - } else if (extension instanceof ArtifactExtensionDependency aed) { - runtimeExtensions.add(ArtifactKey.ga(aed.getDeploymentModule().getGroupId(), + } else if (extension instanceof ArtifactExtensionDependency) { + final ArtifactExtensionDependency aed = (ArtifactExtensionDependency) extension; + + runtimeExtensions.add(new AppArtifactKey(aed.getDeploymentModule().getGroupId(), aed.getDeploymentModule().getArtifactId())); } } @@ -99,12 +103,12 @@ private List collectRuntimeExtensionsDeploymentKeys(Set findExtensionInConfiguration(Set deploymentArtifacts, - List extensions) { - List foundExtensions = new ArrayList<>(); + private List findExtensionInConfiguration(Set deploymentArtifacts, + List extensions) { + List foundExtensions = new ArrayList<>(); for (ResolvedArtifact deploymentArtifact : deploymentArtifacts) { - ArtifactKey key = toArtifactKey(deploymentArtifact.getModuleVersion()); + AppArtifactKey key = toAppArtifactKey(deploymentArtifact.getModuleVersion()); if (extensions.contains(key)) { foundExtensions.add(key); } @@ -112,21 +116,21 @@ private List findExtensionInConfiguration(Set dep return foundExtensions; } - private void printValidationErrors(List invalidRuntimeArtifacts, - List missingDeploymentArtifacts) { + private void printValidationErrors(List invalidRuntimeArtifacts, + List missingDeploymentArtifacts) { Logger log = getLogger(); log.error("Quarkus Extension Dependency Verification Error"); if (!invalidRuntimeArtifacts.isEmpty()) { log.error("The following deployment artifact(s) appear on the runtime classpath: "); - for (ArtifactKey invalidRuntimeArtifact : invalidRuntimeArtifacts) { + for (AppArtifactKey invalidRuntimeArtifact : invalidRuntimeArtifacts) { log.error("- " + invalidRuntimeArtifact); } } if (!missingDeploymentArtifacts.isEmpty()) { log.error("The following deployment artifact(s) were found to be missing in the deployment module: "); - for (ArtifactKey missingDeploymentArtifact : missingDeploymentArtifacts) { + for (AppArtifactKey missingDeploymentArtifact : missingDeploymentArtifacts) { log.error("- " + missingDeploymentArtifact); } } @@ -134,7 +138,7 @@ private void printValidationErrors(List invalidRuntimeArtifacts, throw new GradleException("Quarkus Extension Dependency Verification Error. See logs below"); } - private static ArtifactKey toArtifactKey(ResolvedModuleVersion artifactId) { - return ArtifactKey.ga(artifactId.getId().getGroup(), artifactId.getId().getName()); + private static AppArtifactKey toAppArtifactKey(ResolvedModuleVersion artifactId) { + return new AppArtifactKey(artifactId.getId().getGroup(), artifactId.getId().getName()); } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifact.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifact.java new file mode 100644 index 0000000000000..16a6074f78843 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifact.java @@ -0,0 +1,135 @@ +package io.quarkus.bootstrap.model; + +import java.io.Serializable; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +import io.quarkus.bootstrap.workspace.WorkspaceModule; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.PathCollection; +import io.quarkus.paths.PathList; + +/** + * Represents an application (or its dependency) artifact. + * + * @deprecated in favor of {@link ResolvedDependency} and {@link io.quarkus.maven.dependency.Dependency}. + * + * @author Alexey Loubyansky + */ +@Deprecated(forRemoval = true, since = "3.11.0") +public class AppArtifact extends AppArtifactCoords implements ResolvedDependency, Serializable { + + private static final long serialVersionUID = -6226544163467103712L; + + protected PathsCollection paths; + private final WorkspaceModule module; + private final String scope; + private final int flags; + + public AppArtifact(AppArtifactCoords coords) { + this(coords, null); + } + + public AppArtifact(AppArtifactCoords coords, WorkspaceModule module) { + this(coords.getGroupId(), coords.getArtifactId(), coords.getClassifier(), coords.getType(), coords.getVersion(), + module, "compile", 0); + } + + public AppArtifact(String groupId, String artifactId, String version) { + super(groupId, artifactId, version); + module = null; + scope = "compile"; + flags = 0; + } + + public AppArtifact(String groupId, String artifactId, String classifier, String type, String version) { + super(groupId, artifactId, classifier, type, version); + module = null; + scope = "compile"; + flags = 0; + } + + public AppArtifact(String groupId, String artifactId, String classifier, String type, String version, + WorkspaceModule module, String scope, int flags) { + super(groupId, artifactId, classifier, type, version); + this.module = module; + this.scope = scope; + this.flags = flags; + } + + /** + * @deprecated in favor of {@link #getResolvedPaths()} + */ + @Deprecated + public Path getPath() { + return paths.getSinglePath(); + } + + /** + * Associates the artifact with the given path + * + * @param path artifact location + */ + public void setPath(Path path) { + setPaths(PathsCollection.of(path)); + } + + /** + * Collection of the paths that collectively constitute the artifact's content. + * Normally, especially in the Maven world, an artifact is resolved to a single path, + * e.g. a JAR or a project's output directory. However, in Gradle, depending on the build/test phase, + * artifact's content may need to be represented as a collection of paths. + * + * @return collection of paths that constitute the artifact's content + */ + public PathsCollection getPaths() { + return paths; + } + + /** + * Associates the artifact with a collection of paths that constitute its content. + * + * @param paths collection of paths that constitute the artifact's content. + */ + public void setPaths(PathsCollection paths) { + this.paths = paths; + } + + /** + * Whether the artifact has been resolved, i.e. associated with paths + * that constitute its content. + * + * @return true if the artifact has been resolved, otherwise - false + */ + @Override + public boolean isResolved() { + return paths != null && !paths.isEmpty(); + } + + @Override + public PathCollection getResolvedPaths() { + return paths == null ? null : PathList.from(paths); + } + + @Override + public WorkspaceModule getWorkspaceModule() { + return module; + } + + @Override + public String getScope() { + return scope; + } + + @Override + public int getFlags() { + return flags; + } + + @Override + public Collection getDependencies() { + return List.of(); + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifactCoords.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifactCoords.java new file mode 100644 index 0000000000000..f932ce116e12b --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifactCoords.java @@ -0,0 +1,149 @@ +package io.quarkus.bootstrap.model; + +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import java.util.Objects; + +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.GACT; + +/** + * GroupId, artifactId, classifier, type, version + * + * @deprecated in favor of {@link ArtifactCoords} + * + * @author Alexey Loubyansky + */ +@Deprecated(forRemoval = true, since = "3.11.0") +public class AppArtifactCoords implements ArtifactCoords, Serializable { + + private static final long serialVersionUID = -4401898149727779844L; + + public static final String TYPE_JAR = "jar"; + public static final String TYPE_POM = "pom"; + + public static AppArtifactCoords fromString(String str) { + return new AppArtifactCoords(split(str, new String[5])); + } + + protected static String[] split(String str, String[] parts) { + requireNonNull(str, "str is required"); + final int firstSep = str.indexOf(':'); + final int versionSep = str.lastIndexOf(':'); + if (firstSep < 0) { + throw new IllegalArgumentException( + "Invalid AppArtifactCoords string without any separator: " + str); + } + if (firstSep == versionSep) { + throw new IllegalArgumentException( + "Use AppArtifactKey instead of AppArtifactCoords to deal with 'groupId:artifactId': " + str); + } + if (versionSep <= 0 || versionSep == str.length() - 1) { + throw new IllegalArgumentException("One of type, version or separating them ':' is missing from '" + str + "'"); + } + parts[4] = str.substring(versionSep + 1); + return GACT.split(str, parts, versionSep); + } + + protected final String groupId; + protected final String artifactId; + protected final String classifier; + protected final String type; + protected final String version; + + protected transient AppArtifactKey key; + + protected AppArtifactCoords(String[] parts) { + groupId = parts[0]; + artifactId = parts[1]; + classifier = parts[2]; + type = parts[3] == null ? TYPE_JAR : parts[3]; + version = parts[4]; + } + + public AppArtifactCoords(AppArtifactKey key, String version) { + this.key = key; + this.groupId = key.getGroupId(); + this.artifactId = key.getArtifactId(); + this.classifier = key.getClassifier(); + this.type = key.getType(); + this.version = version; + } + + public AppArtifactCoords(String groupId, String artifactId, String version) { + this(groupId, artifactId, "", TYPE_JAR, version); + } + + public AppArtifactCoords(String groupId, String artifactId, String type, String version) { + this(groupId, artifactId, "", type, version); + } + + public AppArtifactCoords(String groupId, String artifactId, String classifier, String type, String version) { + this.groupId = groupId; + this.artifactId = artifactId; + this.classifier = classifier == null ? "" : classifier; + this.type = type; + this.version = version; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getClassifier() { + return classifier; + } + + public String getType() { + return type; + } + + public String getVersion() { + return version; + } + + public AppArtifactKey getKey() { + return key == null ? key = new AppArtifactKey(groupId, artifactId, classifier, type) : key; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AppArtifactCoords that = (AppArtifactCoords) o; + return Objects.equals(groupId, that.groupId) && + Objects.equals(artifactId, that.artifactId) && + Objects.equals(classifier, that.classifier) && + Objects.equals(type, that.type) && + Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(groupId, artifactId, classifier, type, version); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + append(buf); + return buf.toString(); + } + + protected StringBuilder append(final StringBuilder buf) { + buf.append(groupId).append(':').append(artifactId).append(':'); + if (classifier != null && !classifier.isEmpty()) { + buf.append(classifier); + } + return buf.append(':').append(type).append(':').append(version); + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifactKey.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifactKey.java new file mode 100644 index 0000000000000..877d41ce04729 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifactKey.java @@ -0,0 +1,139 @@ +package io.quarkus.bootstrap.model; + +import java.io.Serializable; + +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.GACT; + +/** + * GroupId, artifactId and classifier + * + * @deprecated in favor of {@link ArtifactKey} + * + * @author Alexey Loubyansky + */ +@Deprecated(forRemoval = true, since = "3.11.0") +public class AppArtifactKey implements ArtifactKey, Serializable { + + private static final long serialVersionUID = -6758193261385541101L; + + public static AppArtifactKey fromString(String str) { + return new AppArtifactKey(GACT.split(str, new String[4], str.length())); + } + + protected final String groupId; + protected final String artifactId; + protected final String classifier; + protected final String type; + + public AppArtifactKey(String[] parts) { + this.groupId = parts[0]; + this.artifactId = parts[1]; + if (parts.length == 2 || parts[2] == null) { + this.classifier = ""; + } else { + this.classifier = parts[2]; + } + if (parts.length <= 3 || parts[3] == null) { + this.type = "jar"; + } else { + this.type = parts[3]; + } + } + + public AppArtifactKey(String groupId, String artifactId) { + this(groupId, artifactId, null); + } + + public AppArtifactKey(String groupId, String artifactId, String classifier) { + this(groupId, artifactId, classifier, null); + } + + public AppArtifactKey(String groupId, String artifactId, String classifier, String type) { + this.groupId = groupId; + this.artifactId = artifactId; + this.classifier = classifier == null ? "" : classifier; + this.type = type; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getClassifier() { + return classifier; + } + + public String getType() { + return type; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((artifactId == null) ? 0 : artifactId.hashCode()); + result = prime * result + ((classifier == null) ? 0 : classifier.hashCode()); + result = prime * result + ((groupId == null) ? 0 : groupId.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof ArtifactKey)) + return false; + ArtifactKey other = (ArtifactKey) obj; + if (artifactId == null) { + if (other.getArtifactId() != null) + return false; + } else if (!artifactId.equals(other.getArtifactId())) + return false; + if (classifier == null) { + if (other.getClassifier() != null) + return false; + } else if (!classifier.equals(other.getClassifier())) + return false; + if (groupId == null) { + if (other.getGroupId() != null) + return false; + } else if (!groupId.equals(other.getGroupId())) + return false; + if (type == null) { + return other.getType() == null; + } else + return type.equals(other.getType()); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append(groupId).append(':').append(artifactId); + if (!classifier.isEmpty()) { + buf.append(':').append(classifier); + } else if (type != null) { + buf.append(':'); + } + if (type != null) { + buf.append(':').append(type); + } + return buf.toString(); + } + + public String toGacString() { + final StringBuilder buf = new StringBuilder(); + buf.append(groupId).append(':').append(artifactId); + if (!classifier.isEmpty()) { + buf.append(':').append(classifier); + } + return buf.toString(); + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppDependency.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppDependency.java new file mode 100644 index 0000000000000..e59c2761452b1 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppDependency.java @@ -0,0 +1,141 @@ +package io.quarkus.bootstrap.model; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.DependencyFlags; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.PathCollection; + +/** + * @deprecated in favor of {@link ResolvedDependency} + */ +@Deprecated(forRemoval = true, since = "3.11.0") +public class AppDependency implements ResolvedDependency, Serializable { + + private static final long serialVersionUID = 7030281544498286020L; + + private final AppArtifact artifact; + private final String scope; + private int flags; + + public AppDependency(AppArtifact artifact, String scope, int... flags) { + this(artifact, scope, false, flags); + } + + public AppDependency(AppArtifact artifact, String scope, boolean optional, int... flags) { + this.artifact = artifact; + this.scope = scope; + int tmpFlags = optional ? DependencyFlags.OPTIONAL : 0; + for (int f : flags) { + tmpFlags |= f; + } + this.flags = tmpFlags; + } + + public AppArtifact getArtifact() { + return artifact; + } + + @Override + public String getScope() { + return scope; + } + + @Override + public int getFlags() { + return flags; + } + + public void clearFlag(int flag) { + if ((flags & flag) > 0) { + flags ^= flag; + } + } + + @Override + public int hashCode() { + return Objects.hash(artifact, flags, scope); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AppDependency other = (AppDependency) obj; + return Objects.equals(artifact, other.artifact) && flags == other.flags && Objects.equals(scope, other.scope); + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + artifact.append(buf).append('('); + if (isDirect()) { + buf.append("direct "); + } + if (isOptional()) { + buf.append("optional "); + } + if (isWorkspaceModule()) { + buf.append("local "); + } + if (isRuntimeExtensionArtifact()) { + buf.append("extension "); + } + if (isRuntimeCp()) { + buf.append("runtime-cp "); + } + if (isDeploymentCp()) { + buf.append("deployment-cp "); + } + return buf.append(scope).append(')').toString(); + } + + @Override + public String getGroupId() { + return artifact.getGroupId(); + } + + @Override + public String getArtifactId() { + return artifact.getArtifactId(); + } + + @Override + public String getClassifier() { + return artifact.getClassifier(); + } + + @Override + public String getType() { + return artifact.getType(); + } + + @Override + public String getVersion() { + return artifact.getVersion(); + } + + @Override + public ArtifactKey getKey() { + return artifact.getKey(); + } + + @Override + public PathCollection getResolvedPaths() { + return artifact.getResolvedPaths(); + } + + @Override + public Collection getDependencies() { + return List.of(); + } +} diff --git a/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/AppArtifactCoordsTest.java b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/AppArtifactCoordsTest.java new file mode 100644 index 0000000000000..4dcb3c170d1f8 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/AppArtifactCoordsTest.java @@ -0,0 +1,54 @@ +package io.quarkus.bootstrap.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class AppArtifactCoordsTest { + + @Test + void testFails() { + String message = assertThrows(IllegalArgumentException.class, () -> AppArtifactCoords.fromString("test-artifact")) + .getMessage(); + Assertions.assertTrue(message.contains("Invalid AppArtifactCoords string without any separator")); + } + + @Test + void testGAFails() { + String message = assertThrows(IllegalArgumentException.class, + () -> AppArtifactCoords.fromString("io.quarkus:test-artifact")).getMessage(); + Assertions.assertTrue(message.contains("Use AppArtifactKey instead of AppArtifactCoords")); + } + + @Test + void testGAV() { + final AppArtifactCoords appArtifactCoords = AppArtifactCoords.fromString("io.quarkus:test-artifact:1.1"); + assertEquals("io.quarkus", appArtifactCoords.getGroupId()); + assertEquals("test-artifact", appArtifactCoords.getArtifactId()); + assertEquals("1.1", appArtifactCoords.getVersion()); + assertEquals("", appArtifactCoords.getClassifier()); + assertEquals("jar", appArtifactCoords.getType()); + } + + @Test + void testGACV() { + final AppArtifactCoords appArtifactCoords = AppArtifactCoords.fromString("io.quarkus:test-artifact:classif:1.1"); + assertEquals("io.quarkus", appArtifactCoords.getGroupId()); + assertEquals("test-artifact", appArtifactCoords.getArtifactId()); + assertEquals("1.1", appArtifactCoords.getVersion()); + assertEquals("classif", appArtifactCoords.getClassifier()); + assertEquals("jar", appArtifactCoords.getType()); + } + + @Test + void testGACTV() { + final AppArtifactCoords appArtifactCoords = AppArtifactCoords.fromString("io.quarkus:test-artifact:classif:json:1.1"); + assertEquals("io.quarkus", appArtifactCoords.getGroupId()); + assertEquals("test-artifact", appArtifactCoords.getArtifactId()); + assertEquals("1.1", appArtifactCoords.getVersion()); + assertEquals("classif", appArtifactCoords.getClassifier()); + assertEquals("json", appArtifactCoords.getType()); + } +} diff --git a/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java b/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java index c2e9d0c4d0c1e..61fdffab18d13 100644 --- a/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java +++ b/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java @@ -593,7 +593,8 @@ private void completeCodestartArtifact(ObjectMapper mapper, ObjectNode extObject /** * If artifact contains "G:A" the project version is added to have "G:A:V"
- * else the version must be defined either with ${project.version} or hardcoded + * else the version must be defined either with ${project.version} or hardcoded
+ * to be compatible with AppArtifactCoords.fromString * * @param originalArtifact * @param projectVersion From 042f82660a6df69cd97d404f4bfab4501e25a7f4 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 23 Jul 2024 09:20:55 +0300 Subject: [PATCH 05/17] Allow MultipartFormDataOutput to specify items with the same key Closes: #42053 (cherry picked from commit 392d68ce448b39a6bb438ad96433394adb4f224a) --- .../multipart/MultipartOutputResource.java | 1 + ...ipartOutputUsingBlockingEndpointsTest.java | 1 + .../multipart/MultipartMessageBodyWriter.java | 28 +++++++++++-------- .../multipart/MultipartFormDataOutput.java | 24 ++++++++++++++-- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java index 2c0971ff4401a..0bbf3f08a6be5 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java @@ -60,6 +60,7 @@ public RestResponse withFormDataOutput() { MultipartFormDataOutput form = new MultipartFormDataOutput(); form.addFormData("name", RESPONSE_NAME, MediaType.TEXT_PLAIN_TYPE); form.addFormData("part-with-filename", RESPONSE_FILENAME, MediaType.TEXT_PLAIN_TYPE, "file.txt"); + form.addFormData("part-with-filename", RESPONSE_FILENAME, MediaType.TEXT_PLAIN_TYPE, "file2.txt"); form.addFormData("custom-surname", RESPONSE_SURNAME, MediaType.TEXT_PLAIN_TYPE); form.addFormData("custom-status", RESPONSE_STATUS, MediaType.TEXT_PLAIN_TYPE) .getHeaders().putSingle("extra-header", "extra-value"); diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java index a631ec0f25390..d1a1b3032726f 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java @@ -75,6 +75,7 @@ public void testWithFormData() { String body = extractable.asString(); assertContainsValue(body, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); assertContainsValue(body, "part-with-filename", MediaType.TEXT_PLAIN, "filename=\"file.txt\""); + assertContainsValue(body, "part-with-filename", MediaType.TEXT_PLAIN, "filename=\"file2.txt\""); assertContainsValue(body, "custom-surname", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_SURNAME); assertContainsValue(body, "custom-status", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_STATUS); assertContainsValue(body, "active", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_ACTIVE); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java index 6896e96e67225..2c8750777dbba 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java @@ -85,21 +85,27 @@ private void write(MultipartFormDataOutput formDataOutput, String boundary, Outp throws IOException { Charset charset = requestContext.getDeployment().getRuntimeConfiguration().body().defaultCharset(); String boundaryLine = "--" + boundary; - Map parts = formDataOutput.getFormData(); - for (Map.Entry entry : parts.entrySet()) { + Map> parts = formDataOutput.getAllFormData(); + for (var entry : parts.entrySet()) { String partName = entry.getKey(); - PartItem part = entry.getValue(); - Object partValue = part.getEntity(); - if (partValue != null) { - if (isListOf(part, File.class) || isListOf(part, FileDownload.class)) { - List list = (List) partValue; - for (int i = 0; i < list.size(); i++) { - writePart(partName, list.get(i), part, boundaryLine, charset, outputStream, requestContext); + List partItems = entry.getValue(); + if (partItems.isEmpty()) { + continue; + } + for (PartItem part : partItems) { + Object partValue = part.getEntity(); + if (partValue != null) { + if (isListOf(part, File.class) || isListOf(part, FileDownload.class)) { + List list = (List) partValue; + for (int i = 0; i < list.size(); i++) { + writePart(partName, list.get(i), part, boundaryLine, charset, outputStream, requestContext); + } + } else { + writePart(partName, partValue, part, boundaryLine, charset, outputStream, requestContext); } - } else { - writePart(partName, partValue, part, boundaryLine, charset, outputStream, requestContext); } } + } // write boundary: -- ... -- diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java index 3b8212907a3ca..baccd98c55cc5 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/multipart/MultipartFormDataOutput.java @@ -1,7 +1,9 @@ package org.jboss.resteasy.reactive.server.multipart; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import jakarta.ws.rs.core.MediaType; @@ -10,9 +12,26 @@ * Used when a Resource method needs to return a multipart output */ public final class MultipartFormDataOutput { - private final Map parts = new LinkedHashMap<>(); + private final Map> parts = new LinkedHashMap<>(); + /** + * @deprecated use {@link #getAllFormData()} instead + */ + @Deprecated(forRemoval = true) public Map getFormData() { + Map result = new LinkedHashMap<>(); + for (var entry : parts.entrySet()) { + if (entry.getValue().isEmpty()) { + continue; + } + // use the last item inserted as this is the old behavior + int lastIndex = entry.getValue().size() - 1; + result.put(entry.getKey(), entry.getValue().get(lastIndex)); + } + return Collections.unmodifiableMap(result); + } + + public Map> getAllFormData() { return Collections.unmodifiableMap(parts); } @@ -31,7 +50,8 @@ public PartItem addFormData(String key, Object entity, MediaType mediaType, Stri } private PartItem addFormData(String key, PartItem part) { - parts.put(key, part); + List items = parts.computeIfAbsent(key, k -> new ArrayList<>()); + items.add(part); return part; } } From a778fc9ca586038810e77fca2e3b489545fd6d10 Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Tue, 23 Jul 2024 14:41:56 +0300 Subject: [PATCH 06/17] Improve reflection registrations in picocli extension Picocli invokes getDeclaredFields on the declaring classes of annotated fields so we need to register all fields instead of just the annotated ones to avoid `MissingReflectionRegistrationError`s at run-time when using `-H:+ThrowMissingRegistrationErrors` or `--exact-reachability-metadata`. Relates to https://github.com/quarkusio/quarkus/issues/41995 (cherry picked from commit 88579588d23e8da65f39f5197a0020e9515c4f35) --- .../deployment/PicocliNativeImageProcessor.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/extensions/picocli/deployment/src/main/java/io/quarkus/picocli/deployment/PicocliNativeImageProcessor.java b/extensions/picocli/deployment/src/main/java/io/quarkus/picocli/deployment/PicocliNativeImageProcessor.java index fd20931917716..00e1b340d0f53 100644 --- a/extensions/picocli/deployment/src/main/java/io/quarkus/picocli/deployment/PicocliNativeImageProcessor.java +++ b/extensions/picocli/deployment/src/main/java/io/quarkus/picocli/deployment/PicocliNativeImageProcessor.java @@ -16,7 +16,6 @@ import org.jboss.jandex.AnnotationValue.Kind; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; -import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; import org.jboss.jandex.Type; import org.jboss.logging.Logger; @@ -55,7 +54,6 @@ void reflectionConfiguration(CombinedIndexBuildItem combinedIndexBuildItem, DotName.createSimple(CommandLine.Unmatched.class.getName())); Set foundClasses = new HashSet<>(); - Set foundFields = new HashSet<>(); Set typeAnnotationValues = new HashSet<>(); for (DotName analyzedAnnotation : annotationsToAnalyze) { @@ -66,7 +64,6 @@ void reflectionConfiguration(CombinedIndexBuildItem combinedIndexBuildItem, foundClasses.add(target.asClass()); break; case FIELD: - foundFields.add(target.asField()); // This may be class which will be used as Mixin. We need to be sure that Picocli will be able // to initialize those even if they are not beans. foundClasses.add(target.asField().declaringClass()); @@ -94,20 +91,18 @@ void reflectionConfiguration(CombinedIndexBuildItem combinedIndexBuildItem, } } + // Register both declared methods and fields as they are accessed by picocli during initialization foundClasses.forEach(classInfo -> { if (Modifier.isInterface(classInfo.flags())) { nativeImageProxies .produce(new NativeImageProxyDefinitionBuildItem(classInfo.name().toString())); - reflectiveClasses - .produce(ReflectiveClassBuildItem.builder(classInfo.name().toString()).constructors(false).methods() - .build()); + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(classInfo.name().toString()).constructors(false) + .methods().fields().build()); } else { reflectiveClasses - .produce(ReflectiveClassBuildItem.builder(classInfo.name().toString()).methods() - .build()); + .produce(ReflectiveClassBuildItem.builder(classInfo.name().toString()).methods().fields().build()); } }); - foundFields.forEach(fieldInfo -> reflectiveFields.produce(new ReflectiveFieldBuildItem(fieldInfo))); typeAnnotationValues.forEach(type -> reflectiveHierarchies.produce(ReflectiveHierarchyBuildItem .builder(type) .source(PicocliNativeImageProcessor.class.getSimpleName()) From 6531a5a768df3981470157fb55c4a1345932cf7f Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Tue, 23 Jul 2024 11:31:33 +0200 Subject: [PATCH 07/17] Quartz - allow bean based jobs to be interruptable (cherry picked from commit 176649afb2bf8471a318a046a369dd6eaa0a5293) --- .../programmatic/InterruptableJobTest.java | 137 ++++++++++++++++++ .../quarkus/quartz/runtime/CdiAwareJob.java | 22 ++- 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/InterruptableJobTest.java diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/InterruptableJobTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/InterruptableJobTest.java new file mode 100644 index 0000000000000..f7226ab5d5d4b --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/programmatic/InterruptableJobTest.java @@ -0,0 +1,137 @@ +package io.quarkus.quartz.test.programmatic; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.quartz.InterruptableJob; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.quartz.UnableToInterruptJobException; + +import io.quarkus.test.QuarkusUnitTest; + +public class InterruptableJobTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyJob.class) + .addAsResource(new StringAsset("quarkus.scheduler.start-mode=forced"), + "application.properties")); + + @Inject + Scheduler scheduler; + + static final CountDownLatch INTERRUPT_LATCH = new CountDownLatch(1); + static final CountDownLatch EXECUTE_LATCH = new CountDownLatch(1); + + static final CountDownLatch NON_INTERRUPTABLE_EXECUTE_LATCH = new CountDownLatch(1); + static final CountDownLatch NON_INTERRUPTABLE_HOLD_LATCH = new CountDownLatch(1); + + @Test + public void testInterruptableJob() throws InterruptedException { + + String jobKey = "myJob"; + JobKey key = new JobKey(jobKey); + Trigger trigger = TriggerBuilder.newTrigger() + .startNow() + .build(); + + JobDetail job = JobBuilder.newJob(MyJob.class) + .withIdentity(key) + .build(); + + try { + scheduler.scheduleJob(job, trigger); + // wait for job to start executing, then interrupt + EXECUTE_LATCH.await(2, TimeUnit.SECONDS); + scheduler.interrupt(key); + } catch (SchedulerException e) { + throw new RuntimeException(e); + } + + assertTrue(INTERRUPT_LATCH.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testNonInterruptableJob() throws InterruptedException { + + String jobKey = "myNonInterruptableJob"; + JobKey key = new JobKey(jobKey); + Trigger trigger = TriggerBuilder.newTrigger() + .startNow() + .build(); + + JobDetail job = JobBuilder.newJob(MyNonInterruptableJob.class) + .withIdentity(key) + .build(); + + try { + scheduler.scheduleJob(job, trigger); + } catch (SchedulerException e) { + throw new RuntimeException(e); + } + + // wait for job to start executing, then interrupt + NON_INTERRUPTABLE_EXECUTE_LATCH.await(2, TimeUnit.SECONDS); + try { + scheduler.interrupt(key); + fail("Should have thrown UnableToInterruptJobException"); + } catch (UnableToInterruptJobException e) { + // This is expected, release the latch holding the job + NON_INTERRUPTABLE_HOLD_LATCH.countDown(); + } + } + + @ApplicationScoped + static class MyJob implements InterruptableJob { + + @Override + public void execute(JobExecutionContext context) { + EXECUTE_LATCH.countDown(); + try { + // halt execution so that we can interrupt it + INTERRUPT_LATCH.await(4, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public void interrupt() { + INTERRUPT_LATCH.countDown(); + } + } + + @ApplicationScoped + static class MyNonInterruptableJob implements Job { + + @Override + public void execute(JobExecutionContext context) { + NON_INTERRUPTABLE_EXECUTE_LATCH.countDown(); + try { + // halt execution so that we can interrupt it + NON_INTERRUPTABLE_HOLD_LATCH.await(4, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + +} diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/CdiAwareJob.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/CdiAwareJob.java index 23f4065234906..4e02136078ef9 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/CdiAwareJob.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/CdiAwareJob.java @@ -3,10 +3,12 @@ import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Instance; +import org.quartz.InterruptableJob; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.Scheduler; +import org.quartz.UnableToInterruptJobException; import org.quartz.spi.TriggerFiredBundle; /** @@ -15,7 +17,7 @@ * trigger. * We will therefore create a new dependent bean for every trigger and destroy it afterwards. */ -class CdiAwareJob implements Job { +class CdiAwareJob implements InterruptableJob { private final Instance jobInstance; @@ -34,4 +36,22 @@ public void execute(JobExecutionContext context) throws JobExecutionException { } } } + + @Override + public void interrupt() throws UnableToInterruptJobException { + Instance.Handle handle = jobInstance.getHandle(); + // delegate if possible; throw an exception in other cases + if (InterruptableJob.class.isAssignableFrom(handle.getBean().getBeanClass())) { + try { + ((InterruptableJob) handle.get()).interrupt(); + } finally { + if (handle.getBean().getScope().equals(Dependent.class)) { + handle.destroy(); + } + } + } else { + throw new UnableToInterruptJobException("Job " + handle.getBean().getBeanClass() + + " can not be interrupted, since it does not implement " + InterruptableJob.class.getName()); + } + } } From 9139876963847463718ddd02432ffa3b87df1923 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Thu, 18 Jul 2024 14:10:26 +0100 Subject: [PATCH 08/17] Override items from super classes when generating config documentation (cherry picked from commit b3d1add8b3ecff135e8e9d48cd11110b8fb6e7ca) --- .../processor/generate_doc/ConfigDocItemFinder.java | 13 +++++++++++++ .../config/runtime/exporter/OtlpExporterConfig.java | 2 -- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java index a79513b5c46bd..eefb38ee3de47 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java @@ -396,6 +396,19 @@ private List recursivelyFindConfigItems(Element element, String r configDocKey.setJavaDocSiteLink(getJavaDocSiteLink(type)); ConfigDocItem configDocItem = new ConfigDocItem(); configDocItem.setConfigDocKey(configDocKey); + + // If there is already a config item with the same key it comes from a super type, and we need to override it + ConfigDocItem parent = null; + for (ConfigDocItem docItem : configDocItems) { + if (docItem.getConfigDocKey().getKey().equals(configDocKey.getKey())) { + parent = docItem; + break; + } + } + // We many want to merge the metadata, but let's keep this simple for now + if (parent != null) { + configDocItems.remove(parent); + } configDocItems.add(configDocItem); } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterConfig.java index 14b8e93ea5013..d894c3e2edf52 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterConfig.java @@ -5,11 +5,9 @@ import java.util.Optional; import java.util.OptionalInt; -import io.quarkus.runtime.annotations.ConfigGroup; import io.smallrye.config.WithDefault; import io.smallrye.config.WithName; -@ConfigGroup public interface OtlpExporterConfig { String DEFAULT_GRPC_BASE_URI = "http://localhost:4317/"; String DEFAULT_HTTP_BASE_URI = "http://localhost:4318/"; From 7545c502314469589be3c94f1f434420478bdb84 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Mon, 22 Jul 2024 10:16:19 +0100 Subject: [PATCH 09/17] Update core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java Co-authored-by: Guillaume Smet (cherry picked from commit 4a88da165f4f8c1c24070e8be0baccf826400bd6) --- .../annotation/processor/generate_doc/ConfigDocItemFinder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java index eefb38ee3de47..5d6664e5463b6 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java @@ -405,7 +405,7 @@ private List recursivelyFindConfigItems(Element element, String r break; } } - // We many want to merge the metadata, but let's keep this simple for now + // We may want to merge the metadata, but let's keep this simple for now if (parent != null) { configDocItems.remove(parent); } From 9bd96c58bcdaa607fca2e220e96b4ec8bc7812f8 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Tue, 23 Jul 2024 11:14:11 +0100 Subject: [PATCH 10/17] Fix NPE with ConfigDocItem (cherry picked from commit 62aa3fbf24b2f8425b595987dffe1d75a3b4edfa) --- .../annotation/processor/generate_doc/ConfigDocItemFinder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java index 5d6664e5463b6..4070bd79b0273 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/generate_doc/ConfigDocItemFinder.java @@ -400,7 +400,7 @@ private List recursivelyFindConfigItems(Element element, String r // If there is already a config item with the same key it comes from a super type, and we need to override it ConfigDocItem parent = null; for (ConfigDocItem docItem : configDocItems) { - if (docItem.getConfigDocKey().getKey().equals(configDocKey.getKey())) { + if (docItem.getConfigDocKey() != null && docItem.getConfigDocKey().getKey().equals(configDocKey.getKey())) { parent = docItem; break; } From 8957d3084739bdb21d1727fee9a0f5d0d6dee077 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Tue, 23 Jul 2024 14:26:16 +0200 Subject: [PATCH 11/17] Add an important note about the STAT_TLS configuration requiring tls set to false This is a change introduced with the TLS registry. The previous configuration was ambiguous. (cherry picked from commit 48d16427ab4b12defc6b702371323cd556877ff1) --- docs/src/main/asciidoc/mailer-reference.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/main/asciidoc/mailer-reference.adoc b/docs/src/main/asciidoc/mailer-reference.adoc index f90c5ab62a8b6..baf7175894a2d 100644 --- a/docs/src/main/asciidoc/mailer-reference.adoc +++ b/docs/src/main/asciidoc/mailer-reference.adoc @@ -418,6 +418,8 @@ quarkus.mailer.start-tls=REQUIRED quarkus.mailer.trust-all=true ---- +IMPORTANT: To use `START_TLS`, make sure you set `tls` to `false` and `start-tls` to `REQUIRED` or `OPTIONAL`. + === Configuring SSL/TLS To establish a TLS connection, you need to configure a _named_ configuration using the xref:./tls-registry-reference.adoc[TLS registry]: From 97ec5d18b2f01570177cd5c6a14700023f1c3657 Mon Sep 17 00:00:00 2001 From: Cristiano Nicolai <570894+cristianonicolai@users.noreply.github.com> Date: Tue, 23 Jul 2024 17:54:56 +1000 Subject: [PATCH 12/17] Use generated hostname when shared network is enabled (cherry picked from commit 6d0b723d9d654834e54a89d02ebd4b0fd06b3a1f) --- .../mongodb/deployment/DevServicesMongoProcessor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java index 710f60fe8554f..c8506b9511233 100644 --- a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java @@ -313,5 +313,10 @@ public String getReplicaSetUrl(String databaseName) { return super.getReplicaSetUrl(databaseName); } } + + @Override + public String getHost() { + return useSharedNetwork ? hostName : super.getHost(); + } } } From f459f720e46e7d82b279beb2279d9fd40ea1e231 Mon Sep 17 00:00:00 2001 From: brunobat Date: Tue, 23 Jul 2024 10:33:30 +0100 Subject: [PATCH 13/17] Avoid warning on analytics (cherry picked from commit 1594c2bc625e5cdcc08a294450d77863c2bd4d75) --- .../src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java | 2 +- devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java index f8c1c3226b617..dfb8a6df77d12 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java @@ -342,7 +342,7 @@ public void close() throws IOException { })) { return scanner.nextLine(); } catch (Exception e) { - getLogger().warn("Failed to collect user input for analytics", e); + getLogger().debug("Failed to collect user input for analytics", e); return ""; } }); diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index d1aa33d56f2e9..99d2c50c54b4e 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -444,7 +444,7 @@ public void close() throws IOException { })) { return scanner.nextLine(); } catch (Exception e) { - getLog().warn("Failed to collect user input for analytics", e); + getLog().debug("Failed to collect user input for analytics", e); return ""; } }); From c3baf93b8108e9eea5a0f79cfffdab2fca2a24c6 Mon Sep 17 00:00:00 2001 From: mariofusco Date: Tue, 23 Jul 2024 15:08:14 +0200 Subject: [PATCH 14/17] Revert "Replace read/write lock in JarResource to avoid virtual threads pinning" This reverts commit ff4b32bb0d8a1df0ccf1cc86a1e1ae9a840c5b0f. (cherry picked from commit 090d0e613fc82e30ab205fd099bac2a08ef31731) --- .../bootstrap/runner/JarFileReference.java | 172 ------------------ .../quarkus/bootstrap/runner/JarResource.java | 140 +++++++++----- .../bootstrap/runner/RunnerClassLoader.java | 93 ++++------ .../runner/VirtualThreadSupport.java | 52 ------ 4 files changed, 127 insertions(+), 330 deletions(-) delete mode 100644 independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarFileReference.java delete mode 100644 independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/VirtualThreadSupport.java diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarFileReference.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarFileReference.java deleted file mode 100644 index d798f20830900..0000000000000 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarFileReference.java +++ /dev/null @@ -1,172 +0,0 @@ -package io.quarkus.bootstrap.runner; - -import static io.quarkus.bootstrap.runner.VirtualThreadSupport.isVirtualThread; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.jar.JarFile; - -import io.smallrye.common.io.jar.JarFiles; - -public class JarFileReference { - // Guarded by an atomic reader counter that emulate the behaviour of a read/write lock. - // To enable virtual threads compatibility and avoid pinning it is not possible to use an explicit read/write lock - // because the jarFile access may happen inside a native call (for example triggered by the RunnerClassLoader) - // and then it is necessary to avoid blocking on it. - private final JarFile jarFile; - - // The referenceCounter - 1 represents the number of effective readers (#aqcuire - #release), while the first - // reference is used to determine if a close has been required. - // The JarFileReference is created as already acquired and that's why the referenceCounter starts from 2 - private final AtomicInteger referenceCounter = new AtomicInteger(2); - - private JarFileReference(JarFile jarFile) { - this.jarFile = jarFile; - } - - /** - * Increase the readers counter of the jarFile. - * - * @return true if the acquiring succeeded: it's now safe to access and use the inner jarFile. - * false if the jar reference is going to be closed and then no longer usable. - */ - private boolean acquire() { - while (true) { - int count = referenceCounter.get(); - if (count == 0) { - return false; - } - if (referenceCounter.compareAndSet(count, count + 1)) { - return true; - } - } - } - - /** - * Decrease the readers counter of the jarFile. - * If the counter drops to 0 and a release has been requested also closes the jarFile. - * - * @return true if the release also closes the underlying jarFile. - */ - private boolean release(JarResource jarResource) { - while (true) { - int count = referenceCounter.get(); - if (count <= 0) { - throw new IllegalStateException( - "The reference counter cannot be negative, found: " + (referenceCounter.get() - 1)); - } - if (referenceCounter.compareAndSet(count, count - 1)) { - if (count == 1) { - try { - jarFile.close(); - } catch (IOException e) { - // ignore - } finally { - jarResource.jarFileReference.set(null); - } - return true; - } - return false; - } - } - } - - /** - * Ask to close this reference. - * If there are no readers currently accessing the jarFile also close it, otherwise defer the closing when the last reader - * will leave. - */ - void close(JarResource jarResource) { - release(jarResource); - } - - @FunctionalInterface - interface JarFileConsumer { - T apply(JarFile jarFile, Path jarPath, String resource); - } - - static T withJarFile(JarResource jarResource, String resource, JarFileConsumer fileConsumer) { - - // Happy path: the jar reference already exists and it's ready to be used - final var localJarFileRefFuture = jarResource.jarFileReference.get(); - if (localJarFileRefFuture != null && localJarFileRefFuture.isDone()) { - JarFileReference jarFileReference = localJarFileRefFuture.join(); - if (jarFileReference.acquire()) { - return consumeSharedJarFile(jarFileReference, jarResource, resource, fileConsumer); - } - } - - // There's no valid jar reference, so load a new one - - // Platform threads can load the jarfile asynchronously and eventually blocking till not ready - // to avoid loading the same jarfile multiple times in parallel - if (!isVirtualThread()) { - // It's ok to eventually block on a join() here since we're sure this is used only by platform thread - return consumeSharedJarFile(asyncLoadAcquiredJarFile(jarResource).join(), jarResource, resource, fileConsumer); - } - - // Virtual threads needs to load the jarfile synchronously to avoid blocking. This means that eventually - // multiple threads could load the same jarfile in parallel and this duplication has to be reconciled - final var newJarFileRef = syncLoadAcquiredJarFile(jarResource); - if (jarResource.jarFileReference.compareAndSet(localJarFileRefFuture, newJarFileRef) || - jarResource.jarFileReference.compareAndSet(null, newJarFileRef)) { - // The new file reference has been successfully published and can be used by the current and other threads - // The join() cannot be blocking here since the CompletableFuture has been created already completed - return consumeSharedJarFile(newJarFileRef.join(), jarResource, resource, fileConsumer); - } - - // The newly created file reference hasn't been published, so it can be used exclusively by the current virtual thread - return consumeUnsharedJarFile(newJarFileRef, jarResource, resource, fileConsumer); - } - - private static T consumeSharedJarFile(JarFileReference jarFileReference, - JarResource jarResource, String resource, JarFileConsumer fileConsumer) { - try { - return fileConsumer.apply(jarFileReference.jarFile, jarResource.jarPath, resource); - } finally { - jarFileReference.release(jarResource); - } - } - - private static T consumeUnsharedJarFile(CompletableFuture jarFileReferenceFuture, - JarResource jarResource, String resource, JarFileConsumer fileConsumer) { - JarFileReference jarFileReference = jarFileReferenceFuture.join(); - try { - return fileConsumer.apply(jarFileReference.jarFile, jarResource.jarPath, resource); - } finally { - boolean closed = jarFileReference.release(jarResource); - assert !closed; - // Check one last time if the file reference can be published and reused by other threads, otherwise close it - if (!jarResource.jarFileReference.compareAndSet(null, jarFileReferenceFuture)) { - closed = jarFileReference.release(jarResource); - assert closed; - } - } - } - - private static CompletableFuture syncLoadAcquiredJarFile(JarResource jarResource) { - try { - return CompletableFuture.completedFuture(new JarFileReference(JarFiles.create(jarResource.jarPath.toFile()))); - } catch (IOException e) { - throw new RuntimeException("Failed to open " + jarResource.jarPath, e); - } - } - - private static CompletableFuture asyncLoadAcquiredJarFile(JarResource jarResource) { - CompletableFuture newJarFileRef = new CompletableFuture<>(); - do { - if (jarResource.jarFileReference.compareAndSet(null, newJarFileRef)) { - try { - newJarFileRef.complete(new JarFileReference(JarFiles.create(jarResource.jarPath.toFile()))); - return newJarFileRef; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - newJarFileRef = jarResource.jarFileReference.get(); - } while (newJarFileRef == null || !newJarFileRef.join().acquire()); - return newJarFileRef; - } -} diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java index 84ae3b69246a2..8b4da096a891d 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java @@ -11,28 +11,44 @@ import java.security.ProtectionDomain; import java.security.cert.Certificate; import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import io.smallrye.common.io.jar.JarEntries; +import io.smallrye.common.io.jar.JarFiles; /** * A jar resource */ public class JarResource implements ClassLoadingResource { - private volatile ProtectionDomain protectionDomain; private final ManifestInfo manifestInfo; + private final Path jarPath; + + private final Lock readLock; + private final Lock writeLock; + + private volatile ProtectionDomain protectionDomain; - final Path jarPath; - final AtomicReference> jarFileReference = new AtomicReference<>(); + //Guarded by the read/write lock; open/close operations on the JarFile require the exclusive lock, + //while using an existing open reference can use the shared lock. + //If a lock is acquired, and as long as it's owned, we ensure that the zipFile reference + //points to an open JarFile instance, and read operations are valid. + //To close the jar, the exclusive lock must be owned, and reference will be set to null before releasing it. + //Likewise, opening a JarFile requires the exclusive lock. + private volatile JarFile zipFile; public JarResource(ManifestInfo manifestInfo, Path jarPath) { this.manifestInfo = manifestInfo; this.jarPath = jarPath; + final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + this.readLock = readWriteLock.readLock(); + this.writeLock = readWriteLock.writeLock(); } @Override @@ -53,48 +69,38 @@ public void init() { @Override public byte[] getResourceData(String resource) { - return JarFileReference.withJarFile(this, resource, JarResourceDataProvider.INSTANCE); - } - - private static class JarResourceDataProvider implements JarFileReference.JarFileConsumer { - private static final JarResourceDataProvider INSTANCE = new JarResourceDataProvider(); - - @Override - public byte[] apply(JarFile jarFile, Path path, String res) { - ZipEntry entry = jarFile.getEntry(res); + final ZipFile zipFile = readLockAcquireAndGetJarReference(); + try { + ZipEntry entry = zipFile.getEntry(resource); if (entry == null) { return null; } - try (InputStream is = jarFile.getInputStream(entry)) { + try (InputStream is = zipFile.getInputStream(entry)) { byte[] data = new byte[(int) entry.getSize()]; int pos = 0; int rem = data.length; while (rem > 0) { int read = is.read(data, pos, rem); if (read == -1) { - throw new RuntimeException("Failed to read all data for " + res); + throw new RuntimeException("Failed to read all data for " + resource); } pos += read; rem -= read; } return data; } catch (IOException e) { - throw new RuntimeException("Failed to read zip entry " + res, e); + throw new RuntimeException("Failed to read zip entry " + resource, e); } + } finally { + readLock.unlock(); } } @Override public URL getResourceURL(String resource) { - return JarFileReference.withJarFile(this, resource, JarResourceURLProvider.INSTANCE); - } - - private static class JarResourceURLProvider implements JarFileReference.JarFileConsumer { - private static final JarResourceURLProvider INSTANCE = new JarResourceURLProvider(); - - @Override - public URL apply(JarFile jarFile, Path path, String res) { - JarEntry entry = jarFile.getJarEntry(res); + final JarFile jarFile = readLockAcquireAndGetJarReference(); + try { + JarEntry entry = jarFile.getJarEntry(resource); if (entry == null) { return null; } @@ -104,7 +110,15 @@ public URL apply(JarFile jarFile, Path path, String res) { if (realName.endsWith("/")) { realName = realName.substring(0, realName.length() - 1); } - final URL resUrl = getUrl(path, realName); + final URI jarUri = jarPath.toUri(); + // first create a URI which includes both the jar file path and the relative resource name + // and then invoke a toURL on it. The URI reconstruction allows for any encoding to be done + // for the "path" which includes the "realName" + var ssp = new StringBuilder(jarUri.getPath().length() + realName.length() + 2); + ssp.append(jarUri.getPath()); + ssp.append("!/"); + ssp.append(realName); + final URL resUrl = new URI(jarUri.getScheme(), ssp.toString(), null).toURL(); // wrap it up into a "jar" protocol URL //horrible hack to deal with '?' characters in the URL //seems to be the only way, the URI constructor just does not let you handle them in a sane way @@ -122,18 +136,8 @@ public URL apply(JarFile jarFile, Path path, String res) { } catch (MalformedURLException | URISyntaxException e) { throw new RuntimeException(e); } - } - - private static URL getUrl(Path jarPath, String realName) throws MalformedURLException, URISyntaxException { - final URI jarUri = jarPath.toUri(); - // first create a URI which includes both the jar file path and the relative resource name - // and then invoke a toURL on it. The URI reconstruction allows for any encoding to be done - // for the "path" which includes the "realName" - var ssp = new StringBuilder(jarUri.getPath().length() + realName.length() + 2); - ssp.append(jarUri.getPath()); - ssp.append("!/"); - ssp.append(realName); - return new URI(jarUri.getScheme(), ssp.toString(), null).toURL(); + } finally { + readLock.unlock(); } } @@ -147,16 +151,60 @@ public ProtectionDomain getProtectionDomain() { return protectionDomain; } + private JarFile readLockAcquireAndGetJarReference() { + while (true) { + readLock.lock(); + final JarFile zipFileLocal = this.zipFile; + if (zipFileLocal != null) { + //Expected fast path: returns a reference to the open JarFile while owning the readLock + return zipFileLocal; + } else { + //This Lock implementation doesn't allow upgrading a readLock to a writeLock, so release it + //as we're going to need the WriteLock. + readLock.unlock(); + //trigger the JarFile being (re)opened. + ensureJarFileIsOpen(); + //Now since we no longer own any lock, we need to try again to obtain the readLock + //and check for the reference still being valid. + //This exposes us to a race with closing the just-opened JarFile; + //however this should be extremely rare, so we can trust we won't loop much; + //A counter doesn't seem necessary, as in fact we know that methods close() + //and resetInternalCaches() are invoked each at most once, which limits the amount + //of loops here in practice. + } + } + } + + private void ensureJarFileIsOpen() { + writeLock.lock(); + try { + if (this.zipFile == null) { + try { + this.zipFile = JarFiles.create(jarPath.toFile()); + } catch (IOException e) { + throw new RuntimeException("Failed to open " + jarPath, e); + } + } + } finally { + writeLock.unlock(); + } + } + @Override public void close() { - var futureRef = jarFileReference.get(); - if (futureRef != null) { - // The jarfile has been already used and it's going to be removed from the cache, - // so the future must be already completed - var ref = futureRef.getNow(null); - if (ref != null) { - ref.close(this); + writeLock.lock(); + try { + final JarFile zipFileLocal = this.zipFile; + if (zipFileLocal != null) { + try { + this.zipFile = null; + zipFileLocal.close(); + } catch (Throwable e) { + //ignore + } } + } finally { + writeLock.unlock(); } } diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java index 2ff5b966de87a..7917d17b851f0 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java @@ -28,10 +28,6 @@ */ public final class RunnerClassLoader extends ClassLoader { - static { - registerAsParallelCapable(); - } - /** * A map of resources by dir name. Root dir/default package is represented by the empty string */ @@ -107,55 +103,18 @@ public Class loadClass(String name, boolean resolve) throws ClassNotFoundExce continue; } definePackage(packageName, resources); - return defineClass(name, data, resource); - } - } - return getParent().loadClass(name); - } - - private void definePackage(String pkgName, ClassLoadingResource[] resources) { - if ((pkgName != null) && getDefinedPackage(pkgName) == null) { - for (ClassLoadingResource classPathElement : resources) { - ManifestInfo mf = classPathElement.getManifestInfo(); - if (mf != null) { - try { - definePackage(pkgName, mf.getSpecTitle(), - mf.getSpecVersion(), - mf.getSpecVendor(), - mf.getImplTitle(), - mf.getImplVersion(), - mf.getImplVendor(), null); - } catch (IllegalArgumentException e) { - var loaded = getDefinedPackage(pkgName); - if (loaded == null) { - throw e; - } + try { + return defineClass(name, data, 0, data.length, resource.getProtectionDomain()); + } catch (LinkageError e) { + loaded = findLoadedClass(name); + if (loaded != null) { + return loaded; } - return; - } - } - try { - definePackage(pkgName, null, null, null, null, null, null, null); - } catch (IllegalArgumentException e) { - var loaded = getDefinedPackage(pkgName); - if (loaded == null) { throw e; } } } - } - - private Class defineClass(String name, byte[] data, ClassLoadingResource resource) { - Class loaded; - try { - return defineClass(name, data, 0, data.length, resource.getProtectionDomain()); - } catch (LinkageError e) { - loaded = findLoadedClass(name); - if (loaded != null) { - return loaded; - } - throw e; - } + return getParent().loadClass(name); } private void accessingResource(final ClassLoadingResource resource) { @@ -172,33 +131,25 @@ private void accessingResource(final ClassLoadingResource resource) { //it's already on the head of the cache: nothing to be done. return; } - for (int i = 1; i < currentlyBufferedResources.length; i++) { final ClassLoadingResource currentI = currentlyBufferedResources[i]; if (currentI == resource || currentI == null) { //it was already cached, or we found an empty slot: bubble it up by one position to give it a boost - bubbleUpCachedResource(resource, i); + final ClassLoadingResource previous = currentlyBufferedResources[i - 1]; + currentlyBufferedResources[i - 1] = resource; + currentlyBufferedResources[i] = previous; return; } } - // else, we drop one element from the cache, // and inserting the latest resource on the tail: toEvict = currentlyBufferedResources[currentlyBufferedResources.length - 1]; - bubbleUpCachedResource(resource, currentlyBufferedResources.length - 1); + currentlyBufferedResources[currentlyBufferedResources.length - 1] = resource; } - // Finally, release the cache for the dropped element: toEvict.resetInternalCaches(); } - private void bubbleUpCachedResource(ClassLoadingResource resource, int i) { - for (int j = i; j > 0; j--) { - currentlyBufferedResources[j] = currentlyBufferedResources[j - 1]; - } - currentlyBufferedResources[0] = resource; - } - @Override protected URL findResource(String name) { name = sanitizeName(name); @@ -270,6 +221,28 @@ protected Enumeration findResources(String name) { return Collections.enumeration(urls); } + private void definePackage(String pkgName, ClassLoadingResource[] resources) { + if ((pkgName != null) && getPackage(pkgName) == null) { + synchronized (getClassLoadingLock(pkgName)) { + if (getPackage(pkgName) == null) { + for (ClassLoadingResource classPathElement : resources) { + ManifestInfo mf = classPathElement.getManifestInfo(); + if (mf != null) { + definePackage(pkgName, mf.getSpecTitle(), + mf.getSpecVersion(), + mf.getSpecVendor(), + mf.getImplTitle(), + mf.getImplVersion(), + mf.getImplVendor(), null); + return; + } + } + definePackage(pkgName, null, null, null, null, null, null, null); + } + } + } + } + private String getPackageNameFromClassName(String className) { final int index = className.lastIndexOf('.'); if (index == -1) { diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/VirtualThreadSupport.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/VirtualThreadSupport.java deleted file mode 100644 index 5d6f03a51a3ab..0000000000000 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/VirtualThreadSupport.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.quarkus.bootstrap.runner; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; - -public class VirtualThreadSupport { - - private static final int MAJOR_JAVA_VERSION = majorVersionFromJavaSpecificationVersion(); - - private static final MethodHandle virtualMh = MAJOR_JAVA_VERSION >= 21 ? findVirtualMH() : null; - - private static MethodHandle findVirtualMH() { - try { - return MethodHandles.publicLookup().findVirtual(Thread.class, "isVirtual", - MethodType.methodType(boolean.class)); - } catch (Exception e) { - return null; - } - } - - static boolean isVirtualThread() { - if (virtualMh == null) { - return false; - } - try { - return (boolean) virtualMh.invokeExact(Thread.currentThread()); - } catch (Throwable t) { - return false; - } - } - - static int majorVersionFromJavaSpecificationVersion() { - return majorVersion(System.getProperty("java.specification.version", "17")); - } - - static int majorVersion(String javaSpecVersion) { - String[] components = javaSpecVersion.split("\\."); - int[] version = new int[components.length]; - - for (int i = 0; i < components.length; ++i) { - version[i] = Integer.parseInt(components[i]); - } - - if (version[0] == 1) { - assert version[1] >= 6; - return version[1]; - } else { - return version[0]; - } - } -} From 0f1f29e46f8421aaa491d5ba88d320d43eec5b36 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Tue, 23 Jul 2024 20:52:35 +0200 Subject: [PATCH 15/17] Fix Redis Pub/Sub subscribeAsMessages method The method was discarding the received items, instead of emitting them downstream. (cherry picked from commit 3743f32b90a2ebdac04d22ff8e2393a323cff45f) --- .../ReactivePubSubCommandsImpl.java | 12 ++++--- .../redis/datasource/PubSubCommandsTest.java | 31 ++++++++++++++++++- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactivePubSubCommandsImpl.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactivePubSubCommandsImpl.java index 10a92a867921d..f4125fe4c85c0 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactivePubSubCommandsImpl.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/ReactivePubSubCommandsImpl.java @@ -260,10 +260,14 @@ public Multi> subscribeAsMessages(String... channels) { List list = List.of(channels); return Multi.createFrom().emitter(emitter -> { - subscribe(list, (channel, value) -> new DefaultRedisPubSubMessage<>(value, channel), emitter::complete, - emitter::fail) - .subscribe().with(subscriber -> emitter - .onTermination(() -> subscriber.unsubscribe(channels).subscribe().asCompletionStage())); + subscribe(list, + (channel, value) -> emitter.emit(new DefaultRedisPubSubMessage<>(value, channel)), + emitter::complete, emitter::fail) + .subscribe().with(x -> { + emitter.onTermination(() -> { + x.unsubscribe(channels).subscribe().asCompletionStage(); + }); + }, emitter::fail); }); } diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/PubSubCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/PubSubCommandsTest.java index 38de5c949ef73..e568395493858 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/PubSubCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/PubSubCommandsTest.java @@ -40,7 +40,7 @@ void initialize() { ds = new BlockingRedisDataSourceImpl(vertx, redis, api, Duration.ofSeconds(5)); pubsub = ds.pubsub(Person.class); - ReactiveRedisDataSourceImpl reactiveDS = new ReactiveRedisDataSourceImpl(vertx, redis, api); + var reactiveDS = new ReactiveRedisDataSourceImpl(vertx, redis, api); reactive = reactiveDS.pubsub(Person.class); } @@ -371,6 +371,35 @@ void subscribeToSingleWithMultiAsMessages() { } + @Test + void testSubscribeAsMessages() { + List> people = new CopyOnWriteArrayList<>(); + Multi> multi = reactive.subscribeAsMessages(channel); + + Cancellable cancellable = multi.subscribe().with(people::add); + + pubsub.publish("foo", new Person("luke", "skywalker")); + pubsub.publish(channel, new Person("luke", "skywalker")); + + Awaitility.await().until(() -> people.size() == 1); + + pubsub.publish(channel, new Person("leia", "skywalker")); + pubsub.publish(channel, new Person("leia", "skywalker")); + pubsub.publish(channel, new Person("leia", "skywalker")); + + Awaitility.await().until(() -> people.size() == 4); + + assertThat(people).allSatisfy(m -> { + assertThat(m.getChannel()).isNotBlank(); + assertThat(m.getPayload()).isNotNull(); + }); + + cancellable.cancel(); + + awaitNoMoreActiveChannels(); + + } + @Test void unsubscribe() { From 46a907ab167c3179eeed25db870d8dc339459656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Tue, 23 Jul 2024 11:32:15 +0200 Subject: [PATCH 16/17] Clarify allowed suffix for the log rotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #42068 Signed-off-by: Martin Bartoš (cherry picked from commit 7c39bb3110b03df13b896ed6950e10bfd3482281) --- .../src/main/java/io/quarkus/runtime/logging/FileConfig.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java index 69152905f18cf..1ba1be87d240a 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/FileConfig.java @@ -2,6 +2,7 @@ import java.io.File; import java.nio.charset.Charset; +import java.time.format.DateTimeFormatter; import java.util.Optional; import java.util.logging.Level; @@ -84,6 +85,8 @@ public static class RotationConfig { * The file handler rotation file suffix. * When used, the file will be rotated based on its suffix. *

+ * The suffix must be in a date-time format that is understood by {@link DateTimeFormatter}. + *

* Example fileSuffix: .yyyy-MM-dd *

* Note: If the suffix ends with .zip or .gz, the rotation file will also be compressed. From ee5856dc29df1a4a61196876804ac5f53fe351ce Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 15 Jul 2024 11:00:49 +0300 Subject: [PATCH 17/17] Fix issue with Jib and mutable jar rebuild Fixes: #41797 (cherry picked from commit 18ab7879d231fec464574767b6022ad5373e356b) --- .../image/jib/deployment/JibProcessor.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index 44b3a109c820e..38d9bf90817ad 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -453,9 +453,10 @@ private JibContainerBuilder createContainerBuilderFromFastJar(String baseJvmImag entrypoint = List.of(RUN_JAVA_PATH); envVars.put("JAVA_APP_JAR", workDirInContainer + "/" + JarResultBuildStep.QUARKUS_RUN_JAR); envVars.put("JAVA_APP_DIR", workDirInContainer.toString()); - envVars.put("JAVA_OPTS_APPEND", String.join(" ", determineEffectiveJvmArguments(jibConfig, appCDSResult))); + envVars.put("JAVA_OPTS_APPEND", + String.join(" ", determineEffectiveJvmArguments(jibConfig, appCDSResult, isMutableJar))); } else { - List effectiveJvmArguments = determineEffectiveJvmArguments(jibConfig, appCDSResult); + List effectiveJvmArguments = determineEffectiveJvmArguments(jibConfig, appCDSResult, isMutableJar); List argsList = new ArrayList<>(3 + effectiveJvmArguments.size()); argsList.add("java"); argsList.addAll(effectiveJvmArguments); @@ -693,7 +694,8 @@ private void mayInheritEntrypoint(JibContainerBuilder jibContainerBuilder, List< } private List determineEffectiveJvmArguments(ContainerImageJibConfig jibConfig, - Optional appCDSResult) { + Optional appCDSResult, + boolean isMutableJar) { List effectiveJvmArguments = new ArrayList<>(jibConfig.jvmArguments); jibConfig.jvmAdditionalArguments.ifPresent(effectiveJvmArguments::addAll); if (appCDSResult.isPresent()) { @@ -708,6 +710,10 @@ private List determineEffectiveJvmArguments(ContainerImageJibConfig jibC effectiveJvmArguments.add("-XX:SharedArchiveFile=" + appCDSResult.get().getAppCDS().getFileName().toString()); } } + if (isMutableJar) { + // see https://github.com/quarkusio/quarkus/issues/41797 + effectiveJvmArguments.add("-Dquarkus.package.output-directory=${PWD}"); + } return effectiveJvmArguments; } @@ -746,7 +752,7 @@ private JibContainerBuilder createContainerBuilderFromLegacyJar(String baseJvmIm // when there is no custom entry point, we just set everything up for a regular java run if (!jibConfig.jvmEntrypoint.isPresent()) { javaContainerBuilder - .addJvmFlags(determineEffectiveJvmArguments(jibConfig, Optional.empty())) + .addJvmFlags(determineEffectiveJvmArguments(jibConfig, Optional.empty(), false)) .setMainClass(mainClassBuildItem.getClassName()); }