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 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/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..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 @@ -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() != 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 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()); + } + } } 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 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; } }