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() != null && docItem.getConfigDocKey().getKey().equals(configDocKey.getKey())) {
+ parent = docItem;
+ break;
+ }
+ }
+ // We may 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/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.
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/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/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 "";
}
});
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]:
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;
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());
}
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();
+ }
}
}
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/";
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())
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 extends Job> jobInstance;
@@ -34,4 +36,22 @@ public void execute(JobExecutionContext context) throws JobExecutionException {
}
}
}
+
+ @Override
+ public void interrupt() throws UnableToInterruptJobException {
+ Instance.Handle extends Job> 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());
+ }
+ }
}
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}
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() {
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/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/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];
- }
- }
-}
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
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