diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index d8e5dfa5af4497..3217d4787efdb1 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -1184,7 +1184,7 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJson(needs.calculate-test-jobs.outputs.native_matrix) }} - runs-on: ${{matrix.os-name}} + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 07f3499ebe3e48..6ed8fa7d562cca 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -17,6 +17,6 @@ io.quarkus.develocity quarkus-project-develocity-extension - 1.1.3 + 1.1.4 diff --git a/bom/application/pom.xml b/bom/application/pom.xml index f2fcc26d1c0cab..c3a990a3307b4b 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -20,10 +20,10 @@ 1.0.19 5.0.0 3.0.2 - 3.2.0 + 3.2.1 1.3.2 1 - 1.1.6 + 1.1.7 2.1.5.Final 3.1.3.Final 6.2.9.Final @@ -147,7 +147,7 @@ 2.13.14 1.2.3 - 3.12.0 + 3.13.0 2.17.0 3.0.0 3.1.0 @@ -160,7 +160,7 @@ 3.2.0 4.2.1 3.0.6.Final - 10.16.0 + 10.17.0 3.0.3 4.27.0 @@ -198,7 +198,7 @@ 1.11.3 2.5.10.Final 0.1.18.Final - 1.20.0 + 1.20.1 3.4.0 2.0.2 @@ -6443,16 +6443,11 @@ io.opentelemetry.semconv opentelemetry-semconv ${opentelemetry-semconv.version} - - - io.opentelemetry - opentelemetry-bom - - - io.opentelemetry - opentelemetry-api - - + + + io.opentelemetry.semconv + opentelemetry-semconv-incubating + ${opentelemetry-semconv.version} diff --git a/build-parent/pom.xml b/build-parent/pom.xml index d1e227acdf8586..2c8533ab5a5622 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -28,7 +28,7 @@ ${scala-maven-plugin.version} - 3.2.0 + 3.2.1 1.0.0 2.5.13 @@ -107,13 +107,13 @@ 7.3.0 - 2.35.0 + 2.36.0 2.0.0 - 0.44.0 + 0.45.0 3.7.0 @@ -539,36 +539,6 @@ jandex-maven-plugin ${jandex.version} - - net.revelc.code.formatter - formatter-maven-plugin - - - quarkus-ide-config - io.quarkus - ${project.version} - - - - - .cache/formatter-maven-plugin-${formatter-maven-plugin.version} - eclipse-format.xml - LF - ${format.skip} - - - - net.revelc.code - impsort-maven-plugin - - - .cache/impsort-maven-plugin-${impsort-maven-plugin.version} - java.,javax.,jakarta.,org.,com. - * - ${format.skip} - true - - maven-resources-plugin diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java new file mode 100644 index 00000000000000..500a84f90b0f17 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java @@ -0,0 +1,45 @@ +package io.quarkus.deployment.logging; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.CompositeIndex; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Contains information to decorate the Log output. Can be used by extensions that output the log / stacktraces, + * for example the error page. + * + * Also see io.quarkus.runtime.logging.DecorateStackUtil to assist with the decoration + */ +public final class LoggingDecorateBuildItem extends SimpleBuildItem { + private final Path srcMainJava; + private final CompositeIndex knowClassesIndex; + + public LoggingDecorateBuildItem(Path srcMainJava, CompositeIndex knowClassesIndex) { + this.srcMainJava = srcMainJava; + this.knowClassesIndex = knowClassesIndex; + } + + public Path getSrcMainJava() { + return srcMainJava; + } + + public CompositeIndex getKnowClassesIndex() { + return knowClassesIndex; + } + + public List getKnowClasses() { + List knowClasses = new ArrayList<>(); + Collection knownClasses = knowClassesIndex.getKnownClasses(); + for (ClassInfo ci : knownClasses) { + knowClasses.add(ci.name().toString()); + } + return knowClasses; + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java index 8781a258c03a6c..af6a515f7be176 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java @@ -1,6 +1,8 @@ package io.quarkus.deployment.logging; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -45,6 +47,8 @@ import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.logging.InitialConfigurator; +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.workspace.WorkspaceModule; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.IsNormal; @@ -86,6 +90,8 @@ import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.metrics.MetricsFactoryConsumerBuildItem; import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.deployment.util.JandexUtil; @@ -102,11 +108,13 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.logging.LoggingFilter; +import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.configuration.ConfigInstantiator; import io.quarkus.runtime.console.ConsoleRuntimeConfig; import io.quarkus.runtime.logging.CategoryBuildTimeConfig; import io.quarkus.runtime.logging.CleanupFilterConfig; +import io.quarkus.runtime.logging.DecorateStackUtil; import io.quarkus.runtime.logging.DiscoveredLogComponents; import io.quarkus.runtime.logging.InheritableLevel; import io.quarkus.runtime.logging.LogBuildTimeConfig; @@ -370,14 +378,25 @@ private DiscoveredLogComponents discoverLogComponents(IndexView index) { void setupStackTraceFormatter(ApplicationArchivesBuildItem item, EffectiveIdeBuildItem ideSupport, BuildSystemTargetBuildItem buildSystemTargetBuildItem, List exceptionNotificationBuildItems, - CuratedApplicationShutdownBuildItem curatedApplicationShutdownBuildItem) { + CuratedApplicationShutdownBuildItem curatedApplicationShutdownBuildItem, + CurateOutcomeBuildItem curateOutcomeBuildItem, + OutputTargetBuildItem outputTargetBuildItem, + LaunchModeBuildItem launchMode, + LogBuildTimeConfig logBuildTimeConfig, + BuildProducer loggingDecorateProducer) { List indexList = new ArrayList<>(); for (ApplicationArchive i : item.getAllApplicationArchives()) { if (i.getResolvedPaths().isSinglePath() && Files.isDirectory(i.getResolvedPaths().getSinglePath())) { indexList.add(i.getIndex()); } } + Path srcMainJava = getSourceRoot(curateOutcomeBuildItem.getApplicationModel(), + outputTargetBuildItem.getOutputDirectory()); + CompositeIndex index = CompositeIndex.create(indexList); + + loggingDecorateProducer.produce(new LoggingDecorateBuildItem(srcMainJava, index)); + //awesome/horrible hack //we know from the index which classes are part of the current application //we add ANSI codes for bold and underline to their names to display them more prominently @@ -393,6 +412,15 @@ public void accept(LogRecord logRecord, Consumer logRecordConsumer) { var elem = stackTrace[i]; if (index.getClassByName(DotName.createSimple(elem.getClassName())) != null) { lastUserCode = stackTrace[i]; + + if (launchMode.getLaunchMode().equals(LaunchMode.DEVELOPMENT) + && logBuildTimeConfig.decorateStacktraces) { + String decoratedString = DecorateStackUtil.getDecoratedString(srcMainJava, elem); + if (decoratedString != null) { + logRecord.setMessage(logRecord.getMessage() + "\n\n" + decoratedString + "\n\n"); + } + } + stackTrace[i] = new StackTraceElement(elem.getClassLoaderName(), elem.getModuleName(), elem.getModuleVersion(), MessageFormat.UNDERLINE + MessageFormat.BOLD + elem.getClassName() @@ -665,6 +693,24 @@ ConsoleCommandBuildItem logConsoleCommand() { return new ConsoleCommandBuildItem(new LogCommand()); } + private Path getSourceRoot(ApplicationModel applicationModel, Path target) { + WorkspaceModule workspaceModule = applicationModel.getAppArtifact().getWorkspaceModule(); + if (workspaceModule != null) { + return workspaceModule.getModuleDir().toPath().resolve(SRC_MAIN_JAVA); + } + + if (target != null) { + var baseDir = target.getParent(); + if (baseDir == null) { + baseDir = target; + } + return baseDir.resolve(SRC_MAIN_JAVA); + } + return Paths.get(SRC_MAIN_JAVA); + } + + private static final String SRC_MAIN_JAVA = "src/main/java"; + @GroupCommandDefinition(name = "log", description = "Logging Commands") public static class LogCommand implements GroupCommand { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java index 48c18f868b7b21..d16e5ff7e94afe 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java @@ -429,13 +429,15 @@ public void run() { } finally { stateLock.unlock(); } - if (currentApplication.isStarted()) { + //take a reliable reference before changing the application state: + final Application app = currentApplication; + if (app.isStarted()) { // On CLI apps, SIGINT won't call io.quarkus.runtime.Application#stop(), // making the awaitShutdown() below block the application termination process // It should be a noop if called twice anyway - currentApplication.stop(); + app.stop(); } - currentApplication.awaitShutdown(); + app.awaitShutdown(); currentApplication = null; System.out.flush(); System.err.flush(); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java index 22b3cce91e9f25..e041748577ffa9 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java @@ -3,11 +3,13 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Scanner; import io.quarkus.dev.config.CurrentConfig; +import io.quarkus.runtime.logging.DecorateStackUtil; import io.quarkus.runtime.util.ExceptionUtil; public class TemplateHtmlBuilder { @@ -146,6 +148,11 @@ public class TemplateHtmlBuilder { private static final String STACKTRACE_DISPLAY_DIV = "
"; + private static final String BRSTI = "___begin_relative_stack_trace_item___"; + private static final String ERSTI = "___end_relative_stack_trace_item___"; + + private static final String OPEN_IDE_LINK = "
"; + private static final String ERROR_STACK = "
\n" + "

The stacktrace below is the original. " + "See the stacktrace in reversed order (root-cause first)

" @@ -159,6 +166,7 @@ public class TemplateHtmlBuilder { "
%1$s
\n" + "
\n"; + private static final String DECORATE_DIV = "
%s
"; private static final String CONFIG_EDITOR_HEAD = "

The following incorrect config values were detected:

" + "
" + "\n"; @@ -218,10 +226,67 @@ public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String } } + public TemplateHtmlBuilder decorate(final Throwable throwable, String srcMainJava, List knowClasses) { + String decoratedString = DecorateStackUtil.getDecoratedString(throwable, srcMainJava, knowClasses); + if (decoratedString != null) { + result.append(String.format(DECORATE_DIV, decoratedString)); + } + + return this; + } + public TemplateHtmlBuilder stack(final Throwable throwable) { - result.append(String.format(ERROR_STACK, escapeHtml(ExceptionUtil.generateStackTrace(throwable)))); - result.append(String.format(ERROR_STACK_REVERSED, escapeHtml(ExceptionUtil.rootCauseFirstStackTrace(throwable)))); - result.append(STACKTRACE_DISPLAY_DIV); + return stack(throwable, List.of()); + } + + public TemplateHtmlBuilder stack(final Throwable throwable, List knowClasses) { + if (knowClasses != null && throwable != null) { + StackTraceElement[] originalStackTrace = Arrays.copyOf(throwable.getStackTrace(), throwable.getStackTrace().length); + StackTraceElement[] stackTrace = throwable.getStackTrace(); + String className = ""; + String type = "java"; //default + int lineNumber = 0; + if (!knowClasses.isEmpty()) { + + for (int i = 0; i < stackTrace.length; ++i) { + var elem = stackTrace[i]; + + className = elem.getClassName(); + String filename = elem.getFileName(); + if (filename != null) { + int dotindex = filename.lastIndexOf("."); + type = elem.getFileName().substring(dotindex + 1); + } + lineNumber = elem.getLineNumber(); + + if (knowClasses.contains(elem.getClassName())) { + stackTrace[i] = new StackTraceElement(elem.getClassLoaderName(), elem.getModuleName(), + elem.getModuleVersion(), + BRSTI + elem.getClassName() + + ERSTI, + elem.getMethodName(), elem.getFileName(), elem.getLineNumber()); + } + } + } + throwable.setStackTrace(stackTrace); + + String original = escapeHtml(ExceptionUtil.generateStackTrace(throwable)); + String rootFirst = escapeHtml(ExceptionUtil.rootCauseFirstStackTrace(throwable)); + if (original.contains(BRSTI)) { + original = original.replace(BRSTI, + String.format(OPEN_IDE_LINK, className, type, lineNumber)); + original = original.replace(ERSTI, "
"); + rootFirst = rootFirst.replace(BRSTI, + String.format(OPEN_IDE_LINK, className, type, lineNumber)); + rootFirst = rootFirst.replace(ERSTI, ""); + } + + result.append(String.format(ERROR_STACK, original)); + result.append(String.format(ERROR_STACK_REVERSED, rootFirst)); + result.append(STACKTRACE_DISPLAY_DIV); + + throwable.setStackTrace(originalStackTrace); + } return this; } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java new file mode 100644 index 00000000000000..c193c4e4334f40 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java @@ -0,0 +1,93 @@ +package io.quarkus.runtime.logging; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +public class DecorateStackUtil { + + public static String getDecoratedString(final Throwable throwable, String srcMainJava, List knowClasses) { + if (srcMainJava != null) { + return DecorateStackUtil.getDecoratedString(throwable, Path.of(srcMainJava), knowClasses); + } + return null; + } + + public static String getDecoratedString(final Throwable throwable, Path srcMainJava, List knowClasses) { + if (knowClasses != null && !knowClasses.isEmpty() && throwable != null) { + StackTraceElement[] stackTrace = throwable.getStackTrace(); + for (int i = 0; i < stackTrace.length; ++i) { + StackTraceElement elem = stackTrace[i]; + if (knowClasses.contains(elem.getClassName())) { + String decoratedString = DecorateStackUtil.getDecoratedString(srcMainJava, elem); + if (decoratedString != null) { + return decoratedString; + } + } + } + } + + return null; + } + + public static String getDecoratedString(Path srcMainJava, StackTraceElement stackTraceElement) { + int lineNumber = stackTraceElement.getLineNumber(); + if (lineNumber > 0 && srcMainJava != null) { + String fullJavaFileName = getFullPath(stackTraceElement.getClassName(), stackTraceElement.getFileName()); + Path f = srcMainJava.resolve(fullJavaFileName); + try { + List contextLines = DecorateStackUtil.getRelatedLinesInSource(f, lineNumber, 2); + if (contextLines != null) { + String header = "Exception in " + stackTraceElement.getFileName() + ":" + stackTraceElement.getLineNumber(); + return header + "\n" + String.join("\n", contextLines); + } + } catch (IOException e) { + // Could not find the source for some reason. Just return nothing then + } + } + return null; + } + + private static List getRelatedLinesInSource(Path filePath, int lineNumber, int contextRange) throws IOException { + if (Files.exists(filePath)) { + List resultLines = new ArrayList<>(); + Deque contextQueue = new ArrayDeque<>(2 * contextRange + 1); + try (BufferedReader reader = Files.newBufferedReader(filePath)) { + String line; + int currentLine = 1; + while ((line = reader.readLine()) != null) { + if (currentLine >= lineNumber - contextRange) { + String ln = String.valueOf(currentLine); + if (currentLine == lineNumber) { + ln = "-> " + ln + " "; + } else { + ln = " " + ln + " "; + } + + contextQueue.add("\t" + ln + line); + } + if (currentLine >= lineNumber + contextRange) { + break; + } + currentLine++; + } + resultLines.addAll(contextQueue); + } + return resultLines; + } + return null; + } + + private static String getFullPath(String fullClassName, String fileName) { + int lastDotIndex = fullClassName.lastIndexOf("."); + String packageName = fullClassName.substring(0, lastDotIndex); + String path = packageName.replace('.', '/'); + return path + "/" + fileName; + } + +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java index 0f9914985fccf9..203a0d7f654b2f 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java @@ -23,6 +23,12 @@ public class LogBuildTimeConfig { @ConfigItem(defaultValue = "DEBUG") public Level minLevel; + /** + * This will decorate the stacktrace in dev mode to show the line in the code that cause the exception + */ + @ConfigItem(defaultValue = "true") + public Boolean decorateStacktraces; + /** * Minimum logging categories. *

diff --git a/core/runtime/src/main/resources/META-INF/template-html-builder.css b/core/runtime/src/main/resources/META-INF/template-html-builder.css index 7760fd6de8743c..6cf8325e64a5eb 100644 --- a/core/runtime/src/main/resources/META-INF/template-html-builder.css +++ b/core/runtime/src/main/resources/META-INF/template-html-builder.css @@ -114,11 +114,28 @@ h3 { display: none; } +.decorate { + font-size: 1.3em; + line-height: 1.5; +} + .stacktrace { font-size: 1.3em; line-height: 1.5; + color: hsla(3, 89%, 42%, 1.0); +} + +.rel-stacktrace-item { + font-weight: 400; + display: inline; } +.rel-stacktrace-item:hover { + text-decoration: underline; + cursor: pointer; +} + + a, a:visited, a:focus, a:active { text-decoration: none; color:hsla(211, 63%, 54%, 1.0); } diff --git a/devtools/project-core-extension-codestarts/pom.xml b/devtools/project-core-extension-codestarts/pom.xml index 61564e2b800dbe..7552c88070c838 100644 --- a/devtools/project-core-extension-codestarts/pom.xml +++ b/devtools/project-core-extension-codestarts/pom.xml @@ -61,6 +61,7 @@ --type basic --no-daemon + --overwrite target/classes/gradle-wrapper true diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index f206e292d6e4fb..e73e092d1ae0c7 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -538,7 +538,7 @@ The project generation has also provided a `Dockerfile.native` in the `src/main/ [source,dockerfile] ---- -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 WORKDIR /work/ RUN chown 1001 /work \ && chmod "g+rwX" /work \ diff --git a/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc b/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc index e65e0cf8e3baa0..e5e5fb99a75390 100644 --- a/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc +++ b/docs/src/main/asciidoc/kafka-schema-registry-json-schema.adoc @@ -76,7 +76,7 @@ Create a file called `src/main/resources/json-schema.json` with the schema for o "type": "string", "description": "The movie's title." }, - "yeay": { + "year": { "type": "integer", "description": "The movie's year." } diff --git a/docs/src/main/asciidoc/opentelemetry-metrics.adoc b/docs/src/main/asciidoc/opentelemetry-metrics.adoc index d2b93e98d33bed..e7a45bfd829c02 100644 --- a/docs/src/main/asciidoc/opentelemetry-metrics.adoc +++ b/docs/src/main/asciidoc/opentelemetry-metrics.adoc @@ -19,7 +19,6 @@ metrics for interactive web applications. - If you search more information about OpenTelemetry Tracing, please refer to the xref:opentelemetry-tracing.adoc[OpenTelemetry Tracing Guide]. ==== - == Prerequisites :prerequisites-docker-compose: @@ -40,7 +39,8 @@ The solution is located in the `opentelemetry-quickstart` link:{quickstarts-tree == Creating the Maven project -First, we need a new project. Create a new project with the following command: +First, we need a new project. +Create a new project with the following command: :create-app-artifact-id: opentelemetry-quickstart :create-app-extensions: rest,quarkus-opentelemetry @@ -114,13 +114,13 @@ public class MetricResource { } ---- -Quarkus is not currently producing metrics out of the box. Here we are creating a counter for the number of invocations of the `hello()` method. +Quarkus is not currently producing metrics out of the box. +Here we are creating a counter for the number of invocations of the `hello()` method. <1> Constructor injection of the `Meter` instance. <2> Create a `LongCounter` named `hello-metrics` with a description and unit. <3> Increment the counter by one for each invocation of the `hello()` method. - === Create the configuration There are no mandatory configurations for the extension to work. @@ -135,9 +135,12 @@ quarkus.otel.exporter.otlp.metrics.endpoint=http://localhost:4317 // <3> quarkus.otel.exporter.otlp.metrics.headers=authorization=Bearer my_secret // <4> ---- -<1> All metrics created from the application will include an OpenTelemetry `Resource` indicating the metrics was created by the `myservice` application. If not set, it will default to the artifact id. -<2> Enable the OpenTelemetry metrics. Must be set at build time. -<3> gRPC endpoint to send the metrics. If not set, it will default to `http://localhost:4317`. +<1> All metrics created from the application will include an OpenTelemetry `Resource` indicating the metrics was created by the `myservice` application. +If not set, it will default to the artifact id. +<2> Enable the OpenTelemetry metrics. +Must be set at build time. +<3> gRPC endpoint to send the metrics. +If not set, it will default to `http://localhost:4317`. <4> Optional gRPC headers commonly used for authentication. To configure the connection using the same properties for all signals, please check the base xref:opentelemetry.adoc#create-the-configuration[configuration section of the OpenTelemetry guide]. @@ -153,7 +156,8 @@ First we need to start a system to visualise the OpenTelemetry data. ==== Grafana-OTel-LGTM dev service You can use the xref:observability-devservices-lgtm.adoc[Grafana-OTel-LGTM] devservice. -This Dev service includes a Grafana for visualizing data, Loki to store logs, Tempo to store traces and Prometheus to store metrics. Also provides and OTel collector to receive the data. +This Dev service includes a Grafana for visualizing data, Loki to store logs, Tempo to store traces and Prometheus to store metrics. +Also provides and OTel collector to receive the data. ==== Logging exporter @@ -164,12 +168,16 @@ quarkus.otel.metrics.exporter=logging <1> quarkus.otel.metric.export.interval=10000ms <2> ---- -<1> Set the exporter to `logging`. Normally you don't need to set this. The default is `cdi`. -<2> Set the interval to export the metrics. The default is `1m`, which is too long for debugging. +<1> Set the exporter to `logging`. +Normally you don't need to set this. +The default is `cdi`. +<2> Set the interval to export the metrics. +The default is `1m`, which is too long for debugging. === Start the application -Now we are ready to run our application. If using `application.properties` to configure the tracer: +Now we are ready to run our application. +If using `application.properties` to configure the tracer: include::{includes}/devtools/dev.adoc[] @@ -187,7 +195,8 @@ $ curl http://localhost:8080/hello-metrics hello-metrics ---- -When using the logger exporter, metrics will be printed to the console. This is a pretty printed example: +When using the logger exporter, metrics will be printed to the console. +This is a pretty printed example: [source,json] ---- { @@ -260,8 +269,10 @@ When using the logger exporter, metrics will be printed to the console. This is <1> Resource attributes common to all telemetry data. <2> Instrumentation scope is allways `io.quarkus.opentelemetry` <3> The name, description and unit of the metric you defined in the constructor of the `MetricResource` class. -<4> The value of the metric. 3 invocations were made until now. -<5> Exemplars additional tracing information about the metric. In this case, the traceId and spanId of one os the request that triggered the metric, since it was last sent. +<4> The value of the metric. +3 invocations were made until now. +<5> Exemplars additional tracing information about the metric. +In this case, the traceId and spanId of one os the request that triggered the metric, since it was last sent. Hit `CTRL+C` or type `q` to stop the application. @@ -269,11 +280,14 @@ Hit `CTRL+C` or type `q` to stop the application. === OpenTelemetry Metrics vs Micrometer Metrics -Metrics are single numerical measurements, often have additional data captured with them. This ancillary data is used to group or aggregate metrics for analysis. +Metrics are single numerical measurements, often have additional data captured with them. +This ancillary data is used to group or aggregate metrics for analysis. Pretty much like in the xref:telemetry-micrometer.adoc#create-your-own-metrics[Quarkus Micrometer extension], you can create your own metrics using the OpenTelemetry API and the concepts are analogous. -The OpenTelemetry API provides a `Meter` interface to create metrics instead of a Registry. The `Meter` interface is the entry point for creating metrics. It provides methods to create counters, gauges, and histograms. +The OpenTelemetry API provides a `Meter` interface to create metrics instead of a Registry. +The `Meter` interface is the entry point for creating metrics. +It provides methods to create counters, gauges, and histograms. Attributes can be added to metrics to add dimensions, pretty much like tags in Micrometer. @@ -322,20 +336,26 @@ counter.add(1, // <2> <1> Create a `LongCounter` named `hello-metrics` with a description and unit. <2> Increment the counter by one. -<3> Add an attribute to the counter. This will create a dimension called `attribute.name` with value `attribute value`. +<3> Add an attribute to the counter. +This will create a dimension called `attribute.name` with value `attribute value`. -IMPORTANT: Each unique combination of metric name and dimension produces a unique time series. Using an unbounded set of dimensional data (many different values like a userId) can lead to a "cardinality explosion", an exponential increase in the creation of new time series. Avoid! +IMPORTANT: Each unique combination of metric name and dimension produces a unique time series. +Using an unbounded set of dimensional data (many different values like a userId) can lead to a "cardinality explosion", an exponential increase in the creation of new time series. +Avoid! OpenTelemetry provides many other types of Counters: `LongUpDownCounter`, `DoubleCounter`, `DoubleUpDownCounter` and also Observable, async counters like `ObservableLongCounter`, `ObservableDoubleCounter`, `ObservableLongUpDownCounter` and `ObservableDoubleUpDownCounter`. For more details please refer to the https://opentelemetry.io/docs/languages/java/instrumentation/#using-counters[OpenTelemetry Java documentation about Counters]. === Gauges -Observable Gauges should be used to measure non-additive values. A value that can increase or decrease over time, like the speedometer on a car. Gauges can be useful when monitoring the statistics for a cache or collection. +Observable Gauges should be used to measure non-additive values. +A value that can increase or decrease over time, like the speedometer on a car. +Gauges can be useful when monitoring the statistics for a cache or collection. -With this metric you provide a function to be periodically probed by a callback. The value returned by the function is the value of the gauge. +With this metric you provide a function to be periodically probed by a callback. +The value returned by the function is the value of the gauge. -The default gauge records `Double`values, but if you want to record `Long` values, you can use +The default gauge records `Double` values, but if you want to record `Long` values, you can use [source, java] ---- @@ -351,7 +371,8 @@ meter.gaugeBuilder("jvm.memory.total") // <1> ---- <1> Create a `Gauge` named `jvm.memory.total` with a description and unit. <2> If you want to record `Long` values you need this builder method because the default gauge records `Double` values. -<3> Build the gauge with a callback. An imperative builder is also available. +<3> Build the gauge with a callback. +An imperative builder is also available. <4> Register the function to call to get the value of the gauge. <5> No added attributes, this time. @@ -360,7 +381,6 @@ Histograms are synchronous instruments used to measure a distribution of values It is intended for statistics such as histograms, summaries, and percentile. The request duration and response payload size are good uses for a histogram. - On this section we have a new class, the `HistogramResource` that will create a `LongHistogram`. [source, java] @@ -412,9 +432,11 @@ public class HistogramResource { ---- <1> Create a `LongHistogram` named `hello.roll.dice` with a description and unit. <2> If you want to record `Long` values you need this builder method because the default histogram records `Double` values. -<3> Set the explicit bucket boundaries for the histogram. The boundaries are inclusive. +<3> Set the explicit bucket boundaries for the histogram. +The boundaries are inclusive. <4> Record the value of the roll. -<5> Add an attribute to the histogram. This will create a dimension called `attribute.name` with value `value`. +<5> Add an attribute to the histogram. +This will create a dimension called `attribute.name` with value `value`. IMPORTANT: Beware of cardinality explosion. @@ -425,7 +447,8 @@ $ curl http://localhost:8080/roll-dice 2 ---- -If we execute 4 consecutive requests, with results *2,2,3 and 4* this will produce the following output. The `Resource` and `InstrumentationScopeInfo` data are ignored for brevity. +If we execute 4 consecutive requests, with results *2,2,3 and 4* this will produce the following output. +The `Resource` and `InstrumentationScopeInfo` data are ignored for brevity. [source,json] ---- //... @@ -477,13 +500,16 @@ data=ImmutableHistogramData{ <7> The maximum value recorded. <8> The explicit bucket boundaries for the histogram. <9> The number of values recorded in each bucket. -<10> The list of exemplars with tracing data for the values recorded. We only show 1 of 3 exemplars for brevity. +<10> The list of exemplars with tracing data for the values recorded. +We only show 1 of 3 exemplars for brevity. <11> One of the 2 calls made with the value 2. === Differences with the Micrometer API -- Timers and Distribution Summaries are not available in the OpenTelemetry API. Instead, use Histograms. -- The OpenTelemetry API does not define annotations for metrics like Micrometer's `@Counted`, `@Timed` or `@MeterTag` You need to manually create the metrics. +- Timers and Distribution Summaries are not available in the OpenTelemetry API. + Instead, use Histograms. +- The OpenTelemetry API does not define annotations for metrics like Micrometer's `@Counted`, `@Timed` or `@MeterTag`. + You need to manually create the metrics. - OpenTelemetry uses their own https://opentelemetry.io/docs/specs/semconv/[Semantic Conventions] to name metrics and attributes. === Resource @@ -491,7 +517,8 @@ See the main xref:opentelemetry.adoc#resource[OpenTelemetry Guide resources] sec == Additional instrumentation -Automatic metrics are not yet provided by the Quarkus OpenTelemetry extension. We plan to bridge the existing Quarkus Micrometer extension metrics to OpenTelemetry in the future. +Automatic metrics are not yet provided by the Quarkus OpenTelemetry extension. +We plan to bridge the existing Quarkus Micrometer extension metrics to OpenTelemetry in the future. == Exporters See the main xref:opentelemetry.adoc#exporters[OpenTelemetry Guide exporters] section. diff --git a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc index ba9f0ff6c8b9ab..c9640697f42acc 100644 --- a/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc +++ b/docs/src/main/asciidoc/quarkus-runtime-base-image.adoc @@ -39,7 +39,7 @@ In this case, you need to use a multi-stage `dockerfile` to copy the required li [source, dockerfile] ---- # First stage - install the dependencies in an intermediate container -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 as BUILD +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 as BUILD RUN microdnf install freetype # Second stage - copy the dependencies @@ -62,7 +62,7 @@ If you need to have access to the full AWT support, you need more than just `lib [source, dockerfile] ---- # First stage - install the dependencies in an intermediate container -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 as BUILD +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 as BUILD RUN microdnf install freetype fontconfig # Second stage - copy the dependencies @@ -112,7 +112,7 @@ To use this base image, use the following `Dockerfile`: [source, dockerfile] ---- -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 WORKDIR /work/ RUN chown 1001 /work \ && chmod "g+rwX" /work \ diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index cafc6486118531..09d9af02496f52 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -1657,6 +1657,34 @@ quarkus.rest-client.my-client.url=... NOTE: MicroProfile REST Client specification does not allow setting proxy credentials. In order to specify proxy user and proxy password programmatically, you need to cast your `RestClientBuilder` to `RestClientBuilderImpl`. +=== Local proxy for dev mode + +When using the REST Client in dev mode, Quarkus has the ability to stand up a pass-through proxy which can be used as a target for Wireshark (or similar tools) +in order to capture all the traffic originating from the REST Client (this really makes sense when the REST Client is used against HTTPS services) + +To enable this feature, all that needs to be done is set the `enable-local-proxy` configuration option for the configKey corresponding to the client for which proxying is desired. +For example: + +[source,properties] +---- +quarkus.rest-client.my-client.enable-local-proxy=true +---- + +When a REST Client does not use a config key (for example when it is created programmatically via `QuarkusRestClientBuilder`) then the class name can be used instead. +For example: + +[source,properties] +---- +quarkus.rest-client."org.acme.SomeClient".enable-local-proxy=true +---- + +The port the proxy is listening can be found in startup logs. An example entry is: + +[source] +---- +Started HTTP proxy server on http://localhost:38227 for REST Client 'org.acme.SomeClient' +---- + == Package and run the application Run the application with: diff --git a/docs/src/main/asciidoc/rest.adoc b/docs/src/main/asciidoc/rest.adoc index f40b865e34917d..7a45e13eac1a92 100644 --- a/docs/src/main/asciidoc/rest.adoc +++ b/docs/src/main/asciidoc/rest.adoc @@ -2271,6 +2271,40 @@ By default, methods annotated with `@ServerExceptionMapper` do **not** run CDI i Users however can opt into interceptors by adding the corresponding annotations to the method. ==== +[TIP] +==== +When mapping an exception to a `@ServerExceptionMapper` method, the cause of the exception normally does not come into play. + +However, some exception types in Java only serve as wrappers for other exceptions. Often, checked exceptions are wrapped into `RuntimeException` just to not have them declared in method `throws` parameters. +Working with `CompletionStage` for example, will require `CompletionException`. There are many such exception types that are just wrappers around the real cause of the exception. + +If you wish to make sure your exception mapper is called for your exception type even when it is wrapped by one of those wrapper exceptions, you can use `@UnwrapException` on the exception wrapper type: + +[source,java] +---- +public class MyExceptionWrapper extends RuntimeException { + public MyExceptionWrapper(Exception cause) { + super(cause); + } +} +---- + +If you don't control that exception wrapper type, you can place the annotation on any class and specify the exception wrapper types it applies to as annotation parameter: + +[source,java] +---- +@UnwrapException({CompletionException.class, RuntimeException.class}) +public class Mapper { + + @ServerExceptionMapper + public Response handleMyException(MyException x) { + // ... + } + +} +---- +==== + [NOTE] ==== Εxception mappers defined in REST endpoint classes will only be called if the exception is thrown in the same class. If you want to define global exception mappers, diff --git a/docs/src/main/asciidoc/scheduler-reference.adoc b/docs/src/main/asciidoc/scheduler-reference.adoc index ebfa3a4f345401..943ff8319b1fe4 100644 --- a/docs/src/main/asciidoc/scheduler-reference.adoc +++ b/docs/src/main/asciidoc/scheduler-reference.adoc @@ -100,7 +100,7 @@ For example `"0 15 10 * * ?"` fires at 10:15am every day. [source,java] ---- @Scheduled(cron = "0 15 10 * * ?") -void fireAt10AmEveryDay() { } +void fireAt1015AmEveryDay() { } ---- The syntax used in CRON expressions is controlled by the `quarkus.scheduler.cron-type` property. @@ -359,6 +359,30 @@ class Jobs { ---- <1> The return type `Uni` instructs the scheduler to execute the method on the Vert.x event loop. +=== How to use multiple scheduler implementations + +In some cases, it might be useful to choose a scheduler implementation used to execute a scheduled method. +However, only one `Scheduler` implementation is used for all scheduled methods by default. +For example, the `quarkus-quartz` extension provides an implementation that supports clustering but it also removes the simple in-memory implementation from the game. +Now, if clustering is enabled then it's not possible to define a scheduled method that would be executed locally on a single node. +Nevertheless, if you set the `quarkus.scheduler.use-composite-scheduler` config property to `true` then a composite `Scheduler` is used instead. +This means that multiple scheduler implementations are kept running side by side. +Furthermore, it's possible to chose a specific implementation used to execute a scheduled method using `@Scheduled#executeWith()`. + +[source,java] +---- +class Jobs { + + @Scheduled(cron = "0 15 10 * * ?") <1> + void fireAt10AmEveryDay() { } + + @Scheduled(every = "1s", executeWith = Scheduled.SIMPLE) <2> + void everySecond() { } +} +---- +<1> If the `quarkus-quartz` extension is present then this method will be executed with the Quartz-specific scheduler. +<2> If `quarkus.scheduler.use-composite-scheduler=true` is set then this method will be executed with the simple in-memory implementation provided by the `quarkus-scheduler` extension. + == Scheduler Quarkus provides a built-in bean of type `io.quarkus.scheduler.Scheduler` that can be injected and used to pause/resume the scheduler and individual scheduled methods identified by a specific `Scheduled#identity()`. diff --git a/docs/src/main/asciidoc/spring-data-jpa.adoc b/docs/src/main/asciidoc/spring-data-jpa.adoc index cb41a8bc350d04..52c20b03c83e4b 100644 --- a/docs/src/main/asciidoc/spring-data-jpa.adoc +++ b/docs/src/main/asciidoc/spring-data-jpa.adoc @@ -599,6 +599,7 @@ An extensive list of examples can be seen in the https://github.com/quarkusio/qu * Methods of the `org.springframework.data.repository.query.QueryByExampleExecutor` interface - if any of these are invoked, a Runtime exception will be thrown. * QueryDSL support. No attempt will be made to generate implementations of the QueryDSL related repositories. +* Using `org.springframework.data.jpa.repository.JpaSpecificationExecutor` * Customizing the base repository for all repository interfaces in the code base. ** In Spring Data JPA this is done by registering a class that extends `org.springframework.data.jpa.repository.support.SimpleJpaRepository` however in Quarkus this class is not used at all (since all the necessary plumbing is done at build time). Similar support might be added to Quarkus in the future. diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java index 428b12ad125663..b23ee16b8cb6a9 100644 --- a/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java +++ b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/RedHatOpenJDKRuntimeBaseProviderTest.java @@ -40,10 +40,10 @@ void testImage(int javaVersion, int ubiVersion, String imageVersion) { static Stream imageCombinations() { return Stream.of( - Arguments.of(17, 8, "1.19"), - Arguments.of(21, 8, "1.19"), - Arguments.of(17, 9, "1.18"), - Arguments.of(21, 9, "1.18")); + Arguments.of(17, 8, "1.20"), + Arguments.of(21, 8, "1.20"), + Arguments.of(17, 9, "1.20"), + Arguments.of(21, 9, "1.20")); } @Test diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProviderTest.java b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProviderTest.java index c0ede25661708b..a19842053b3d46 100644 --- a/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProviderTest.java +++ b/extensions/container-image/container-image-docker-common/deployment/src/test/java/io/quarkus/container/image/docker/common/deployment/UbiMinimalBaseProviderTest.java @@ -39,8 +39,8 @@ void testImage(int ubiVersion, int javaVersion, String imageVersion) { static Stream imageCombinations() { return Stream.of( - Arguments.of(8, 17, "8.9"), - Arguments.of(8, 21, "8.9"), + Arguments.of(8, 17, "8.10"), + Arguments.of(8, 21, "8.10"), Arguments.of(9, 17, "9.4"), Arguments.of(9, 21, "9.4")); } diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java17 b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java17 index 77d59a96bc997e..777116ae52f3e4 100644 --- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java17 +++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java17 @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 ARG JAVA_PACKAGE=java-17-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java21 b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java21 index ee1e916b5d106a..acc768a6f9ca17 100644 --- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java21 +++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-java21 @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 ARG JAVA_PACKAGE=java-21-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-17-runtime b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-17-runtime index a06add4a4733ea..e6463565e14821 100644 --- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-17-runtime +++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-17-runtime @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.20 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-21-runtime b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-21-runtime index 0a470b183b8da5..c2dafccd009908 100644 --- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-21-runtime +++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi8-openjdk-21-runtime @@ -1,5 +1,5 @@ # Use Java 21 base image -FROM registry.access.redhat.com/ubi8/openjdk-21-runtime:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-21-runtime:1.20 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-17-runtime b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-17-runtime index bc09884236f94c..f83ad03e05062a 100644 --- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-17-runtime +++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-17-runtime @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi9/openjdk-17-runtime:1.18 +FROM registry.access.redhat.com/ubi9/openjdk-17-runtime:1.20 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-21-runtime b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-21-runtime index d50d4344121960..ffa2b1f33d7343 100644 --- a/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-21-runtime +++ b/extensions/container-image/container-image-docker-common/deployment/src/test/resources/ubi9-openjdk-21-runtime @@ -1,5 +1,5 @@ # Use Java 21 base image -FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.18 +FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.20 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java index a6792a12483669..3e442d337425ed 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/ContainerImageJibConfig.java @@ -17,9 +17,9 @@ public class ContainerImageJibConfig { /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21-runtime:1.19} + * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21-runtime:1.20} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.19} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17-runtime:1.20} is used as the default. */ @ConfigItem public Optional baseJvmImage; 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 38d9bf90817ad0..3b93e633ec9463 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 @@ -98,9 +98,9 @@ public class JibProcessor { private static final String OPENJDK_PREFIX = "openjdk"; private static final String RUNTIME_SUFFIX = "runtime"; - private static final String JAVA_21_BASE_IMAGE = String.format("%s/%s-21-%s:1.19", UBI8_PREFIX, OPENJDK_PREFIX, + private static final String JAVA_21_BASE_IMAGE = String.format("%s/%s-21-%s:1.20", UBI8_PREFIX, OPENJDK_PREFIX, RUNTIME_SUFFIX); - private static final String JAVA_17_BASE_IMAGE = String.format("%s/%s-17-%s:1.19", UBI8_PREFIX, OPENJDK_PREFIX, + private static final String JAVA_17_BASE_IMAGE = String.format("%s/%s-17-%s:1.20", UBI8_PREFIX, OPENJDK_PREFIX, RUNTIME_SUFFIX); // The source for this can be found at https://github.com/jboss-container-images/openjdk/blob/ubi8/modules/run/artifacts/opt/jboss/container/java/run/run-java.sh diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java index df20d64f6c5964..d37b1679db2010 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/ContainerImageOpenshiftConfig.java @@ -15,8 +15,8 @@ @ConfigRoot(name = "openshift", phase = ConfigPhase.BUILD_TIME) public class ContainerImageOpenshiftConfig { - public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.19"; - public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.19"; + public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.20"; + public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.20"; public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; @@ -47,9 +47,9 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion * The value of this property is used to create an ImageStream for the builder image used in the Openshift build. * When it references images already available in the internal Openshift registry, the corresponding streams are used * instead. - * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.19} + * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.20} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.19} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.20} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java index 675519cd28f9a6..7fe3ac81cffba1 100644 --- a/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java +++ b/extensions/container-image/container-image-openshift/deployment/src/main/java/io/quarkus/container/image/openshift/deployment/S2iConfig.java @@ -12,8 +12,8 @@ @ConfigRoot(phase = ConfigPhase.BUILD_TIME) public class S2iConfig { - public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.19"; - public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.19"; + public static final String DEFAULT_BASE_JVM_JDK17_IMAGE = "registry.access.redhat.com/ubi8/openjdk-17:1.20"; + public static final String DEFAULT_BASE_JVM_JDK21_IMAGE = "registry.access.redhat.com/ubi8/openjdk-21:1.20"; public static final String DEFAULT_BASE_NATIVE_IMAGE = "quay.io/quarkus/ubi-quarkus-native-binary-s2i:2.0"; public static final String DEFAULT_NATIVE_TARGET_FILENAME = "application"; @@ -41,9 +41,9 @@ public static String getDefaultJvmImage(CompiledJavaVersionBuildItem.JavaVersion /** * The base image to be used when a container image is being produced for the jar build. * - * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.19} + * When the application is built against Java 21 or higher, {@code registry.access.redhat.com/ubi8/openjdk-21:1.20} * is used as the default. - * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.19} is used as the default. + * Otherwise {@code registry.access.redhat.com/ubi8/openjdk-17:1.20} is used as the default. */ @ConfigItem public Optional baseJvmImage; diff --git a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java index 83b262b1470310..127d583a8bd3c1 100644 --- a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java +++ b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java @@ -64,10 +64,9 @@ import io.quarkus.quartz.runtime.jdbc.QuarkusPostgreSQLDelegate; import io.quarkus.quartz.runtime.jdbc.QuarkusStdJDBCDelegate; import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.deployment.SchedulerImplementationBuildItem; -/** - * - */ public class QuartzProcessor { private static final DotName JOB = DotName.createSimple(Job.class.getName()); @@ -77,6 +76,11 @@ FeatureBuildItem feature() { return new FeatureBuildItem(Feature.QUARTZ); } + @BuildStep + SchedulerImplementationBuildItem implementation() { + return new SchedulerImplementationBuildItem(Scheduled.QUARTZ, DotName.createSimple(QuartzSchedulerImpl.class), 1); + } + @BuildStep AdditionalBeanBuildItem beans() { return new AdditionalBeanBuildItem(QuartzSchedulerImpl.class); diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/DependentBeanJobTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/DependentBeanJobTest.java index 9a2943c1ab78ac..410018b410a2b6 100644 --- a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/DependentBeanJobTest.java +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/DependentBeanJobTest.java @@ -86,10 +86,10 @@ public void testDependentBeanJobDestroyed() throws SchedulerException, Interrupt @Test public void testDependentBeanJobWithRefire() throws SchedulerException, InterruptedException { - // 5 one-off jobs should trigger construction/execution/destruction 10 times in total - CountDownLatch execLatch = service.initExecuteLatch(10); - CountDownLatch constructLatch = service.initConstructLatch(10); - CountDownLatch destroyedLatch = service.initDestroyedLatch(10); + // 5 one-off jobs should trigger construction/execution/destruction 5 times in total + CountDownLatch execLatch = service.initExecuteLatch(5); + CountDownLatch constructLatch = service.initConstructLatch(5); + CountDownLatch destroyedLatch = service.initDestroyedLatch(5); for (int i = 0; i < 5; i++) { Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("myTrigger" + i, "myRefiringGroup") @@ -104,10 +104,10 @@ public void testDependentBeanJobWithRefire() throws SchedulerException, Interrup assertTrue(constructLatch.await(2, TimeUnit.SECONDS), "Latch count: " + constructLatch.getCount()); assertTrue(destroyedLatch.await(2, TimeUnit.SECONDS), "Latch count: " + destroyedLatch.getCount()); - // repeating job triggering three times; we expect six beans to exist for that due to refires - execLatch = service.initExecuteLatch(6); - constructLatch = service.initConstructLatch(6); - destroyedLatch = service.initDestroyedLatch(6); + // repeating job triggering three times; re-fires should NOT recreate the bean instance + execLatch = service.initExecuteLatch(3); + constructLatch = service.initConstructLatch(3); + destroyedLatch = service.initDestroyedLatch(3); JobDetail job = JobBuilder.newJob(RefiringJob.class) .withIdentity("myRepeatingJob", "myRefiringGroup") .build(); diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeJobDefinitionTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeJobDefinitionTest.java new file mode 100644 index 00000000000000..3f0a82a31277f3 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeJobDefinitionTest.java @@ -0,0 +1,108 @@ +package io.quarkus.quartz.test.composite; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.quartz.QuartzScheduler; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.runtime.Constituent; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.common.annotation.Identifier; + +public class CompositeJobDefinitionTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + }) + .overrideConfigKey("quarkus.scheduler.use-composite-scheduler", "true") + .overrideConfigKey("quarkus.scheduler.start-mode", "forced"); + + @Constituent + QuartzScheduler quartz; + + @Constituent + @Identifier("SIMPLE") + Scheduler simple; + + @Inject + Scheduler composite; + + static CountDownLatch simpleLatch = new CountDownLatch(1); + static CountDownLatch quartzLatch = new CountDownLatch(1); + static CountDownLatch autoLatch = new CountDownLatch(1); + + static void reset() { + simpleLatch = new CountDownLatch(1); + quartzLatch = new CountDownLatch(1); + autoLatch = new CountDownLatch(1); + } + + @Test + public void testExecution() throws InterruptedException { + + assertEquals("Scheduler implementation not available: bar", + assertThrows(IllegalArgumentException.class, () -> composite.newJob("foo").setExecuteWith("bar")).getMessage()); + + composite.newJob("simple") + .setInterval("1s") + .setExecuteWith(Scheduled.SIMPLE) + .setTask(se -> { + simpleLatch.countDown(); + }).schedule(); + + composite.newJob("quartz") + .setInterval("1s") + .setExecuteWith(Scheduled.QUARTZ) + .setTask(se -> { + quartzLatch.countDown(); + }).schedule(); + + composite.newJob("auto") + .setInterval("1s") + .setTask(se -> { + autoLatch.countDown(); + }).schedule(); + + assertTrue(simpleLatch.await(5, TimeUnit.SECONDS)); + assertTrue(quartzLatch.await(5, TimeUnit.SECONDS)); + assertTrue(autoLatch.await(5, TimeUnit.SECONDS)); + + assertNull(quartz.getScheduledJob("simple")); + assertNotNull(quartz.getScheduledJob("quartz")); + assertNotNull(quartz.getScheduledJob("auto")); + + assertNotNull(simple.getScheduledJob("simple")); + assertNull(simple.getScheduledJob("quartz")); + assertNull(simple.getScheduledJob("auto")); + + assertNotNull(composite.getScheduledJob("quartz")); + assertNotNull(composite.getScheduledJob("auto")); + assertNotNull(composite.getScheduledJob("simple")); + + composite.pause(); + reset(); + assertFalse(composite.isRunning()); + assertFalse(simpleLatch.await(2, TimeUnit.SECONDS)); + + composite.resume(); + assertTrue(composite.isRunning()); + assertTrue(simpleLatch.await(5, TimeUnit.SECONDS)); + assertTrue(quartzLatch.await(5, TimeUnit.SECONDS)); + assertTrue(autoLatch.await(5, TimeUnit.SECONDS)); + } + +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerNotUsedTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerNotUsedTest.java new file mode 100644 index 00000000000000..08a8dd58a85b9f --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerNotUsedTest.java @@ -0,0 +1,36 @@ +package io.quarkus.quartz.test.composite; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class CompositeSchedulerNotUsedTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Jobs.class)) + .assertException(t -> { + assertThat(t).cause().isInstanceOf(IllegalStateException.class) + .hasMessageContaining( + "The required scheduler implementation is not available because the composite scheduler is not used: SIMPLE"); + }); + + @Test + public void test() { + fail(); + } + + static class Jobs { + + @Scheduled(every = "1s", executeWith = Scheduled.SIMPLE) + void quartz() { + } + + } +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerTest.java new file mode 100644 index 00000000000000..d08607b05e317e --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/composite/CompositeSchedulerTest.java @@ -0,0 +1,99 @@ +package io.quarkus.quartz.test.composite; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.quartz.QuartzScheduler; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.runtime.Constituent; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.common.annotation.Identifier; + +public class CompositeSchedulerTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Jobs.class)) + .overrideConfigKey("quarkus.scheduler.use-composite-scheduler", "true"); + + @Constituent + QuartzScheduler quartz; + + @Constituent + @Identifier("SIMPLE") + Scheduler simple; + + @Inject + Scheduler composite; + + @Test + public void testExecution() throws InterruptedException { + assertTrue(Jobs.simpleLatch.await(5, TimeUnit.SECONDS)); + assertTrue(Jobs.quartzLatch.await(5, TimeUnit.SECONDS)); + assertTrue(Jobs.autoLatch.await(5, TimeUnit.SECONDS)); + + assertNull(quartz.getScheduledJob("simple")); + assertNotNull(quartz.getScheduledJob("quartz")); + assertNotNull(quartz.getScheduledJob("auto")); + + assertNotNull(simple.getScheduledJob("simple")); + assertNull(simple.getScheduledJob("quartz")); + assertNull(simple.getScheduledJob("auto")); + + assertNotNull(composite.getScheduledJob("quartz")); + assertNotNull(composite.getScheduledJob("auto")); + assertNotNull(composite.getScheduledJob("simple")); + + composite.pause(); + Jobs.reset(); + assertFalse(composite.isRunning()); + assertFalse(Jobs.simpleLatch.await(2, TimeUnit.SECONDS)); + + composite.resume(); + assertTrue(composite.isRunning()); + assertTrue(Jobs.simpleLatch.await(5, TimeUnit.SECONDS)); + assertTrue(Jobs.quartzLatch.await(5, TimeUnit.SECONDS)); + assertTrue(Jobs.autoLatch.await(5, TimeUnit.SECONDS)); + } + + static class Jobs { + + static CountDownLatch simpleLatch = new CountDownLatch(1); + static CountDownLatch quartzLatch = new CountDownLatch(1); + static CountDownLatch autoLatch = new CountDownLatch(1); + + static void reset() { + simpleLatch = new CountDownLatch(1); + quartzLatch = new CountDownLatch(1); + autoLatch = new CountDownLatch(1); + } + + @Scheduled(identity = "simple", every = "1s", executeWith = Scheduled.SIMPLE) + void simple() { + simpleLatch.countDown(); + } + + @Scheduled(identity = "quartz", every = "1s", executeWith = Scheduled.QUARTZ) + void quartz() { + quartzLatch.countDown(); + } + + @Scheduled(identity = "auto", every = "1s") + void auto() { + autoLatch.countDown(); + } + + } +} 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 index f7226ab5d5d4b9..8381694a248cca 100644 --- 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 @@ -6,6 +6,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -40,6 +41,7 @@ public class InterruptableJobTest { static final CountDownLatch INTERRUPT_LATCH = new CountDownLatch(1); static final CountDownLatch EXECUTE_LATCH = new CountDownLatch(1); + static Integer initCounter = 0; static final CountDownLatch NON_INTERRUPTABLE_EXECUTE_LATCH = new CountDownLatch(1); static final CountDownLatch NON_INTERRUPTABLE_HOLD_LATCH = new CountDownLatch(1); @@ -66,7 +68,9 @@ public void testInterruptableJob() throws InterruptedException { throw new RuntimeException(e); } - assertTrue(INTERRUPT_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(INTERRUPT_LATCH.await(3, TimeUnit.SECONDS)); + // asserts that a single dep. scoped bean instance was used for both, execute() and interrupt() methods + assertTrue(initCounter == 1); } @Test @@ -102,6 +106,11 @@ public void testNonInterruptableJob() throws InterruptedException { @ApplicationScoped static class MyJob implements InterruptableJob { + @PostConstruct + public void postConstruct() { + initCounter++; + } + @Override public void execute(JobExecutionContext context) { EXECUTE_LATCH.countDown(); 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 4e02136078ef94..8c3e42c1becd07 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 @@ -19,19 +19,24 @@ */ class CdiAwareJob implements InterruptableJob { - private final Instance jobInstance; + private final Instance.Handle handle; + private final Job beanInstance; - public CdiAwareJob(Instance jobInstance) { - this.jobInstance = jobInstance; + public CdiAwareJob(Instance.Handle handle) { + this.handle = handle; + this.beanInstance = handle.get(); } @Override public void execute(JobExecutionContext context) throws JobExecutionException { - Instance.Handle handle = jobInstance.getHandle(); + boolean refire = false; try { - handle.get().execute(context); + beanInstance.execute(context); + } catch (JobExecutionException e) { + refire = e.refireImmediately(); + throw e; } finally { - if (handle.getBean().getScope().equals(Dependent.class)) { + if (refire != true && handle.getBean().getScope().equals(Dependent.class)) { handle.destroy(); } } @@ -39,16 +44,9 @@ 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(); - } - } + ((InterruptableJob) beanInstance).interrupt(); } else { throw new UnableToInterruptJobException("Job " + handle.getBean().getBeanClass() + " can not be interrupted, since it does not implement " + InterruptableJob.class.getName()); diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java index 7e08b2a596de56..57a79077eb6882 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java @@ -198,7 +198,8 @@ public QuartzSchedulerImpl(SchedulerContext context, QuartzSupport quartzSupport if (!enabled) { LOGGER.info("Quartz scheduler is disabled by config property and will not be started"); this.scheduler = null; - } else if (!forceStart && context.getScheduledMethods().isEmpty() && !context.forceSchedulerStart()) { + } else if (!forceStart && context.getScheduledMethods(Scheduled.QUARTZ).isEmpty() + && !context.forceSchedulerStart()) { LOGGER.info("No scheduled business methods found - Quartz scheduler will not be started"); this.scheduler = null; } else { @@ -232,10 +233,13 @@ public org.quartz.Trigger apply(TriggerKey triggerKey) { } }; - for (ScheduledMethod method : context.getScheduledMethods()) { + for (ScheduledMethod method : context.getScheduledMethods(Scheduled.QUARTZ)) { int nameSequence = 0; for (Scheduled scheduled : method.getSchedules()) { + if (!context.matchesImplementation(scheduled, Scheduled.QUARTZ)) { + continue; + } String identity = SchedulerUtils.lookUpPropertyValue(scheduled.identity()); if (identity.isEmpty()) { identity = ++nameSequence + "_" + method.getInvokerClassName(); @@ -345,6 +349,11 @@ public org.quartz.Scheduler getScheduler() { return scheduler; } + @Override + public String implementation() { + return Scheduled.QUARTZ; + } + @Override public void pause() { if (!enabled) { @@ -893,7 +902,7 @@ public Trigger schedule() { } scheduled = true; SyntheticScheduled scheduled = new SyntheticScheduled(identity, cron, every, 0, TimeUnit.MINUTES, delayed, - overdueGracePeriod, concurrentExecution, skipPredicate, timeZone); + overdueGracePeriod, concurrentExecution, skipPredicate, timeZone, implementation); return createJobDefinitionQuartzTrigger(this, scheduled, null); } @@ -1246,7 +1255,7 @@ public Job newJob(TriggerFiredBundle bundle, org.quartz.Scheduler Scheduler) thr Instance instance = jobs.select(jobClass); if (instance.isResolvable()) { // This is a job backed by a CDI bean - return jobWithSpanWrapper(new CdiAwareJob(instance)); + return jobWithSpanWrapper(new CdiAwareJob(instance.getHandle())); } // Instantiate a plain job class return jobWithSpanWrapper(super.newJob(bundle, Scheduler)); diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index f40f1eeb724aa9..193fd8f588f78a 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -61,15 +61,17 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem; -import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem; import io.quarkus.arc.deployment.CompletedApplicationClassPredicateBuildItem; import io.quarkus.arc.deployment.QualifierRegistrarBuildItem; +import io.quarkus.arc.deployment.SynthesisFinishedBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; import io.quarkus.arc.processor.Annotations; import io.quarkus.arc.processor.BeanInfo; +import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.InjectionPointInfo; import io.quarkus.arc.processor.QualifierRegistrar; @@ -951,7 +953,7 @@ void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis, BuildProducer incorrectExpressions, BuildProducer implicitClasses, BuildProducer expressionMatches, - BeanDiscoveryFinishedBuildItem beanDiscovery, + SynthesisFinishedBuildItem synthesisFinished, List checkedTemplates, List templateData, QuteConfig config, @@ -970,10 +972,7 @@ public String apply(String id) { return findTemplatePath(templatesAnalysis, id); } }; - // IMPLEMENTATION NOTE: - // We do not support injection of synthetic beans with names - // Dependency on the ValidationPhaseBuildItem would result in a cycle in the build chain - Map namedBeans = beanDiscovery.beanStream().withName() + Map namedBeans = synthesisFinished.beanStream().withName() .collect(toMap(BeanInfo::getName, Function.identity())); // Map implicit class -> set of used members Map> implicitClassToMembersUsed = new HashMap<>(); @@ -2447,9 +2446,7 @@ public boolean test(TypeCheck check) { @BuildStep @Record(value = STATIC_INIT) void initialize(BuildProducer syntheticBeans, QuteRecorder recorder, - List generatedValueResolvers, List templatePaths, - Optional templateVariants, - List templateInitializers, + List templatePaths, Optional templateVariants, TemplateRootsBuildItem templateRoots) { List templates = new ArrayList<>(); @@ -2475,14 +2472,25 @@ void initialize(BuildProducer syntheticBeans, QuteRecord } syntheticBeans.produce(SyntheticBeanBuildItem.configure(QuteContext.class) - .supplier(recorder.createContext(generatedValueResolvers.stream() - .map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()), templates, - tags, variants, templateInitializers.stream() - .map(TemplateGlobalProviderBuildItem::getClassName).collect(Collectors.toList()), + .scope(BuiltinScope.SINGLETON.getInfo()) + .supplier(recorder.createContext(templates, + tags, variants, templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet()), templateContents)) .done()); } + @BuildStep + @Record(value = STATIC_INIT) + void initializeGeneratedClasses(BeanContainerBuildItem beanContainer, QuteRecorder recorder, + List generatedValueResolvers, + List templateInitializers) { + // The generated classes must be initialized after the template expressions are validated in order to break the cycle in the build chain + recorder.initializeGeneratedClasses(generatedValueResolvers.stream() + .map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()), + templateInitializers.stream() + .map(TemplateGlobalProviderBuildItem::getClassName).collect(Collectors.toList())); + } + @BuildStep QualifierRegistrarBuildItem turnLocationIntoQualifier() { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java index e3bca7a111002e..6ba4437a63ec80 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java @@ -14,10 +14,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; import io.quarkus.qute.Qute; import io.quarkus.qute.Template; import io.quarkus.qute.deployment.Hello; import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.common.annotation.Identifier; public class InjectNamespaceResolverTest { @@ -28,7 +32,21 @@ public class InjectNamespaceResolverTest { .addAsResource( new StringAsset( "{inject:hello.ping} != {inject:simple.ping} and {cdi:hello.ping} != {cdi:simple.ping}"), - "templates/foo.html")); + "templates/foo.html")) + .addBuildChainCustomizer(bcb -> { + bcb.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce(SyntheticBeanBuildItem.configure(String.class) + .addQualifier().annotation(Identifier.class).addValue("value", "synthetic").done() + .name("synthetic") + .creator(mc -> { + mc.returnValue(mc.load("Yes!")); + }) + .done()); + } + }).produces(SyntheticBeanBuildItem.class).build(); + }); @Inject Template foo; @@ -45,6 +63,9 @@ public void testInjection() { assertEquals("pong::<br>", Qute.fmt("{cdi:hello.ping}::{newLine}").contentType("text/html").data("newLine", "
").render()); assertEquals(2, SimpleBean.DESTROYS.longValue()); + + // Test a synthetic named bean injected in a template + assertEquals("YES!", Qute.fmt("{cdi:synthetic.toUpperCase}").render()); } @Named("simple") diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java index 2e53fe166580d7..60797098731d9b 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java @@ -5,20 +5,23 @@ import java.util.Set; import java.util.function.Supplier; +import io.quarkus.arc.Arc; import io.quarkus.runtime.annotations.Recorder; @Recorder public class QuteRecorder { - public Supplier createContext(List resolverClasses, - List templatePaths, List tags, Map> variants, - List templateGlobalProviderClasses, Set templateRoots, Map templateContents) { + public Supplier createContext(List templatePaths, List tags, Map> variants, + Set templateRoots, Map templateContents) { return new Supplier() { @Override public Object get() { return new QuteContext() { + volatile List resolverClasses; + volatile List templateGlobalProviderClasses; + @Override public List getTemplatePaths() { return templatePaths; @@ -31,6 +34,9 @@ public List getTags() { @Override public List getResolverClasses() { + if (resolverClasses == null) { + throw generatedClassesNotInitialized(); + } return resolverClasses; } @@ -41,6 +47,9 @@ public Map> getVariants() { @Override public List getTemplateGlobalProviderClasses() { + if (templateGlobalProviderClasses == null) { + throw generatedClassesNotInitialized(); + } return templateGlobalProviderClasses; } @@ -53,11 +62,27 @@ public Set getTemplateRoots() { public Map getTemplateContents() { return templateContents; } + + @Override + public void setGeneratedClasses(List resolverClasses, List templateGlobalProviderClasses) { + this.resolverClasses = resolverClasses; + this.templateGlobalProviderClasses = templateGlobalProviderClasses; + } + + private IllegalStateException generatedClassesNotInitialized() { + return new IllegalStateException("Generated classes not initialized yet!"); + } + }; } }; } + public void initializeGeneratedClasses(List resolverClasses, List templateGlobalProviderClasses) { + QuteContext context = Arc.container().instance(QuteContext.class).get(); + context.setGeneratedClasses(resolverClasses, templateGlobalProviderClasses); + } + public interface QuteContext { List getResolverClasses(); @@ -74,6 +99,15 @@ public interface QuteContext { Map getTemplateContents(); + /** + * The generated classes must be initialized after the template expressions are validated (later during the STATIC_INIT + * bootstrap phase) in order to break the cycle in the build chain. + * + * @param resolverClasses + * @param templateGlobalProviderClasses + */ + void setGeneratedClasses(List resolverClasses, List templateGlobalProviderClasses); + } } diff --git a/extensions/resteasy-classic/rest-client-config/deployment/src/main/java/io/quarkus/restclient/config/deployment/RestClientConfigUtils.java b/extensions/resteasy-classic/rest-client-config/deployment/src/main/java/io/quarkus/restclient/config/deployment/RestClientConfigUtils.java index f46f0befb527d1..66e1a3009690fd 100644 --- a/extensions/resteasy-classic/rest-client-config/deployment/src/main/java/io/quarkus/restclient/config/deployment/RestClientConfigUtils.java +++ b/extensions/resteasy-classic/rest-client-config/deployment/src/main/java/io/quarkus/restclient/config/deployment/RestClientConfigUtils.java @@ -4,11 +4,26 @@ import static io.quarkus.restclient.config.Constants.MP_REST_SCOPE_FORMAT; import static io.quarkus.restclient.config.Constants.QUARKUS_REST_SCOPE_FORMAT; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.eclipse.microprofile.config.Config; import org.jboss.jandex.ClassInfo; +import io.quarkus.deployment.GeneratedClassGizmoAdaptor; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; +import io.quarkus.deployment.builditem.StaticInitConfigBuilderBuildItem; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.restclient.config.AbstractRestClientConfigBuilder; +import io.quarkus.restclient.config.RegisteredRestClient; +import io.quarkus.runtime.configuration.ConfigBuilder; + public final class RestClientConfigUtils { private RestClientConfigUtils() { @@ -50,4 +65,40 @@ public static Optional getDefaultScope(Config config) { return config.getOptionalValue(GLOBAL_REST_SCOPE_FORMAT, String.class); } + public static void generateRestClientConfigBuilder( + List restClients, + BuildProducer generatedClass, + BuildProducer staticInitConfigBuilder, + BuildProducer runTimeConfigBuilder) { + + String className = "io.quarkus.runtime.generated.RestClientConfigBuilder"; + try (ClassCreator classCreator = ClassCreator.builder() + .classOutput(new GeneratedClassGizmoAdaptor(generatedClass, true)) + .className(className) + .superClass(AbstractRestClientConfigBuilder.class) + .interfaces(ConfigBuilder.class) + .setFinal(true) + .build()) { + + MethodCreator method = classCreator.getMethodCreator( + MethodDescriptor.ofMethod(AbstractRestClientConfigBuilder.class, "getRestClients", List.class)); + + ResultHandle list = method.newInstance(MethodDescriptor.ofConstructor(ArrayList.class)); + for (RegisteredRestClient restClient : restClients) { + ResultHandle restClientElement = method.newInstance( + MethodDescriptor.ofConstructor(RegisteredRestClient.class, String.class, String.class, String.class), + method.load(restClient.getFullName()), + method.load(restClient.getSimpleName()), + restClient.getConfigKey() != null ? method.load(restClient.getConfigKey()) : method.loadNull()); + + method.invokeVirtualMethod(MethodDescriptor.ofMethod(ArrayList.class, "add", boolean.class, Object.class), list, + restClientElement); + } + + method.returnValue(list); + } + + staticInitConfigBuilder.produce(new StaticInitConfigBuilderBuildItem(className)); + runTimeConfigBuilder.produce(new RunTimeConfigBuilderBuildItem(className)); + } } diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/AbstractRestClientConfigBuilder.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/AbstractRestClientConfigBuilder.java new file mode 100644 index 00000000000000..f477e6d78e5f13 --- /dev/null +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/AbstractRestClientConfigBuilder.java @@ -0,0 +1,40 @@ +package io.quarkus.restclient.config; + +import java.util.List; + +import io.quarkus.runtime.configuration.ConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilder; + +/** + * Registers and force load REST Client configuration. + *

+ * Usually, named configuration is mapped using a Map because the names are dynamic and unknown to + * Quarkus. In the case of the REST Client, configuration names are fixed and known at build time, but not to the point + * where the names can be mapped statically, so they still need to be mapped in a Map. + *

+ * To populate a Map, because the names are dynamic, the Config system has to rely on the list of + * property names provided by each source. This also applies to the REST Client, but since the names are known to + * Quarkus, the REST Client configuration could be loaded even for sources that don't provide a list of property + * names. To achieve such behaviour, we provide a dummy configuration under each REST Client name to force + * the Config system to look up the remaining configuration in the same tree. + *

+ * The concrete implementation is bytecode generated in + * io.quarkus.restclient.config.deployment.RestClientConfigUtils#generateRestClientConfigBuilder + */ +public abstract class AbstractRestClientConfigBuilder implements ConfigBuilder { + @Override + public SmallRyeConfigBuilder configBuilder(final SmallRyeConfigBuilder builder) { + List restClients = getRestClients(); + builder.withInterceptors(new RestClientNameFallbackConfigSourceInterceptor(restClients)); + for (RegisteredRestClient restClient : restClients) { + builder.withDefaultValue("quarkus.rest-client.\"" + restClient.getFullName() + "\".force", "true"); + builder.withDefaultValue("quarkus.rest-client." + restClient.getSimpleName() + ".force", "true"); + if (restClient.getConfigKey() != null) { + builder.withDefaultValue("quarkus.rest-client." + restClient.getConfigKey() + ".force", "true"); + } + } + return builder; + } + + public abstract List getRestClients(); +} diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/QueryParamStyleConverter.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/QueryParamStyleConverter.java deleted file mode 100644 index 9c25f4075291f5..00000000000000 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/QueryParamStyleConverter.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.quarkus.restclient.config; - -import org.eclipse.microprofile.config.spi.Converter; -import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; - -public class QueryParamStyleConverter implements Converter { - @Override - public QueryParamStyle convert(String value) throws IllegalArgumentException, NullPointerException { - return QueryParamStyle.valueOf(value); - } -} diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RegisteredRestClient.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RegisteredRestClient.java new file mode 100644 index 00000000000000..a350fcd9d8b9cd --- /dev/null +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RegisteredRestClient.java @@ -0,0 +1,29 @@ +package io.quarkus.restclient.config; + +public class RegisteredRestClient { + private final String fullName; + private final String simpleName; + private final String configKey; + + public RegisteredRestClient(final String fullName, final String simpleName) { + this(fullName, simpleName, null); + } + + public RegisteredRestClient(final String fullName, final String simpleName, final String configKey) { + this.fullName = fullName; + this.simpleName = simpleName; + this.configKey = configKey; + } + + public String getFullName() { + return fullName; + } + + public String getSimpleName() { + return simpleName; + } + + public String getConfigKey() { + return configKey; + } +} diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientBuildConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientBuildConfig.java deleted file mode 100644 index d24deacf258333..00000000000000 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientBuildConfig.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.quarkus.restclient.config; - -import java.util.Optional; - -import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; - -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; - -@ConfigGroup -public class RestClientBuildConfig { - - /** - * The CDI scope to use for injection. This property can contain either a fully qualified class name of a CDI scope - * annotation (such as "jakarta.enterprise.context.ApplicationScoped") or its simple name (such as - * "ApplicationScoped"). - * By default, this is not set which means the interface is not registered as a bean unless it is annotated with - * {@link RegisterRestClient}. - * If an interface is not annotated with {@link RegisterRestClient} and this property is set, then Quarkus will make the - * interface - * a bean of the configured scope. - */ - @ConfigItem - public Optional scope; - - /** - * If set to true, then Quarkus will ensure that all calls from the rest client go through a local proxy - * server (that is managed by Quarkus). - * This can be very useful for capturing network traffic to a service that use HTTPS. - *

- * This property is not applicable to the RESTEasy Client, only the Quarkus Rest client (formerly RESTEasy Reactive client). - *

- * This property only applicable to dev and test mode. - */ - @ConfigItem(defaultValue = "false") - public boolean enableLocalProxy; - - /** - * This setting is used to select which proxy provider to use if there are multiple ones. - * It only applies if {@code enable-local-proxy} is true. - *

- * The algorithm for picking between multiple provider is the following: - *

    - *
  • If only the default is around, use it (it's name is {@code default})
  • - *
  • If there is only one besides the default, use it
  • - *
  • If there are multiple ones, fail
  • - *
- */ - public Optional localProxyProvider; -} diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientBuilderFactory.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientBuilderFactory.java index 3e9222f6f41062..0e3df7e6b64071 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientBuilderFactory.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientBuilderFactory.java @@ -2,18 +2,22 @@ import java.util.ServiceLoader; +import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.rest.client.RestClientBuilder; +import io.smallrye.config.SmallRyeConfig; + /** * Factory which creates MicroProfile RestClientBuilder instance configured according to current Quarkus application * configuration. - * + *

* The builder instance can be further tweaked, if needed, before building the rest client proxy. */ public interface RestClientBuilderFactory { default RestClientBuilder newBuilder(Class proxyType) { - return newBuilder(proxyType, RestClientsConfig.getInstance()); + return newBuilder(proxyType, + ConfigProvider.getConfig().unwrap(SmallRyeConfig.class).getConfigMapping(RestClientsConfig.class)); } RestClientBuilder newBuilder(Class proxyType, RestClientsConfig restClientsConfigRoot); diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java deleted file mode 100644 index 3165fdb1c643ce..00000000000000 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java +++ /dev/null @@ -1,463 +0,0 @@ -package io.quarkus.restclient.config; - -import java.util.Collections; -import java.util.Map; -import java.util.Optional; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; -import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; - -import io.quarkus.runtime.annotations.ConfigDocDefault; -import io.quarkus.runtime.annotations.ConfigDocMapKey; -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.configuration.MemorySize; -import io.smallrye.config.SmallRyeConfig; - -@ConfigGroup -public class RestClientConfig { - - public static final RestClientConfig EMPTY; - - static { - EMPTY = new RestClientConfig(); - EMPTY.url = Optional.empty(); - EMPTY.uri = Optional.empty(); - EMPTY.overrideUri = Optional.empty(); - EMPTY.providers = Optional.empty(); - EMPTY.connectTimeout = Optional.empty(); - EMPTY.readTimeout = Optional.empty(); - EMPTY.followRedirects = Optional.empty(); - EMPTY.multipartPostEncoderMode = Optional.empty(); - EMPTY.proxyAddress = Optional.empty(); - EMPTY.proxyUser = Optional.empty(); - EMPTY.proxyPassword = Optional.empty(); - EMPTY.nonProxyHosts = Optional.empty(); - EMPTY.queryParamStyle = Optional.empty(); - EMPTY.verifyHost = Optional.empty(); - EMPTY.trustStore = Optional.empty(); - EMPTY.trustStorePassword = Optional.empty(); - EMPTY.trustStoreType = Optional.empty(); - EMPTY.keyStore = Optional.empty(); - EMPTY.keyStorePassword = Optional.empty(); - EMPTY.keyStoreType = Optional.empty(); - EMPTY.hostnameVerifier = Optional.empty(); - EMPTY.tlsConfigurationName = Optional.empty(); - EMPTY.connectionTTL = Optional.empty(); - EMPTY.connectionPoolSize = Optional.empty(); - EMPTY.keepAliveEnabled = Optional.empty(); - EMPTY.maxRedirects = Optional.empty(); - EMPTY.multipart = new RestClientMultipartConfig(); - EMPTY.multipart.maxChunkSize = Optional.empty(); - EMPTY.headers = Collections.emptyMap(); - EMPTY.shared = Optional.empty(); - EMPTY.name = Optional.empty(); - EMPTY.userAgent = Optional.empty(); - EMPTY.http2 = Optional.empty(); - EMPTY.maxChunkSize = Optional.empty(); - EMPTY.alpn = Optional.empty(); - EMPTY.captureStacktrace = Optional.empty(); - } - - public RestClientMultipartConfig multipart; - - /** - * The base URL to use for this service. This property or the `uri` property is considered required, unless - * the `baseUri` attribute is configured in the `@RegisterRestClient` annotation. - */ - @ConfigItem - public Optional url; - - /** - * The base URI to use for this service. This property or the `url` property is considered required, unless - * the `baseUri` attribute is configured in the `@RegisterRestClient` annotation. - */ - @ConfigItem - public Optional uri; - - /** - * This property is only meant to be set by advanced configurations to override whatever value was set for the uri or url. - * The override is done using the REST Client class name configuration syntax. - *

- * This property is not applicable to the RESTEasy Client, only the Quarkus Rest client (formerly RESTEasy Reactive client). - */ - @ConfigItem - public Optional overrideUri; - - /** - * Map where keys are fully-qualified provider classnames to include in the client, and values are their integer - * priorities. The equivalent of the `@RegisterProvider` annotation. - */ - @ConfigItem - public Optional providers; - - /** - * Timeout specified in milliseconds to wait to connect to the remote endpoint. - */ - @ConfigItem - public Optional connectTimeout; - - /** - * Timeout specified in milliseconds to wait for a response from the remote endpoint. - */ - @ConfigItem - public Optional readTimeout; - - /** - * A boolean value used to determine whether the client should follow HTTP redirect responses. - */ - @ConfigItem - public Optional followRedirects; - - /** - * Mode in which the form data are encoded. Possible values are `HTML5`, `RFC1738` and `RFC3986`. - * The modes are described in the - * Netty - * documentation - *

- * By default, Rest Client Reactive uses RFC1738. - *

- * This property is not applicable to the RESTEasy Client. - */ - @ConfigItem - public Optional multipartPostEncoderMode; - - /** - * A string value in the form of `:` that specifies the HTTP proxy server hostname - * (or IP address) and port for requests of this client to use. - * - * Use `none` to disable proxy - */ - @ConfigItem - public Optional proxyAddress; - - /** - * Proxy username. - *

- * This property is not applicable to the RESTEasy Client. - */ - @ConfigItem - public Optional proxyUser; - - /** - * Proxy password. - *

- * This property is not applicable to the RESTEasy Client. - */ - @ConfigItem - public Optional proxyPassword; - - /** - * Hosts to access without proxy - *

- * This property is not applicable to the RESTEasy Client. - */ - @ConfigItem - public Optional nonProxyHosts; - - /** - * An enumerated type string value with possible values of "MULTI_PAIRS" (default), "COMMA_SEPARATED", - * or "ARRAY_PAIRS" that specifies the format in which multiple values for the same query parameter is used. - */ - @ConfigItem - public Optional queryParamStyle; - - /** - * Set whether hostname verification is enabled. Default is enabled. - * This setting should not be disabled in production as it makes the client vulnerable to MITM attacks. - */ - @ConfigItem - public Optional verifyHost; - - /** - * The trust store location. Can point to either a classpath resource or a file. - */ - @ConfigItem - public Optional trustStore; - - /** - * The trust store password. - */ - @ConfigItem - public Optional trustStorePassword; - - /** - * The type of the trust store. Defaults to "JKS". - */ - @ConfigItem - public Optional trustStoreType; - - /** - * The key store location. Can point to either a classpath resource or a file. - */ - @ConfigItem - public Optional keyStore; - - /** - * The key store password. - */ - @ConfigItem - public Optional keyStorePassword; - - /** - * The type of the key store. Defaults to "JKS". - */ - @ConfigItem - public Optional keyStoreType; - - /** - * The class name of the host name verifier. The class must have a public no-argument constructor. - */ - @ConfigItem - public Optional hostnameVerifier; - - /** - * The name of the TLS configuration to use. - *

- * If not set and the default TLS configuration is configured ({@code quarkus.tls.*}) then that will be used. - * If a name is configured, it uses the configuration from {@code quarkus.tls..*} - * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. - *

- * If no TLS configuration is set, then the keys-tore, trust-store, etc. properties will be used. - *

- * This property is not applicable to the RESTEasy Client. - */ - @ConfigItem - public Optional tlsConfigurationName; - - /** - * The time in ms for which a connection remains unused in the connection pool before being evicted and closed. - * A timeout of {@code 0} means there is no timeout. - */ - @ConfigItem - public Optional connectionTTL; - - /** - * The size of the connection pool for this client. - */ - @ConfigItem - public Optional connectionPoolSize; - - /** - * If set to false disables the keep alive completely. - */ - @ConfigItem - public Optional keepAliveEnabled; - - /** - * The maximum number of redirection a request can follow. - *

- * This property is not applicable to the RESTEasy Client. - */ - @ConfigItem - public Optional maxRedirects; - - /** - * The HTTP headers that should be applied to all requests of the rest client. - *

- * This property is not applicable to the RESTEasy Client. - */ - @ConfigItem - @ConfigDocMapKey("header-name") - public Map headers; - - /** - * Set to true to share the HTTP client between REST clients. - * There can be multiple shared clients distinguished by name, when no specific name is set, - * the name __vertx.DEFAULT is used. - *

- * This property is not applicable to the RESTEasy Client. - */ - @ConfigItem - public Optional shared; - - /** - * Set the HTTP client name, used when the client is shared, otherwise ignored. - *

- * This property is not applicable to the RESTEasy Client. - */ - @ConfigItem - public Optional name; - - /** - * Configure the HTTP user-agent header to use. - *

- * This property is not applicable to the RESTEasy Client. - */ - @ConfigItem - public Optional userAgent; - - /** - * If this is true then HTTP/2 will be enabled. - */ - @ConfigItem - public Optional http2; - - /** - * The max HTTP chunk size (8096 bytes by default). - *

- * This property is not applicable to the RESTEasy Client. - */ - @ConfigItem - @ConfigDocDefault("8K") - public Optional maxChunkSize; - - /** - * If the Application-Layer Protocol Negotiation is enabled, the client will negotiate which protocol to use over the - * protocols exposed by the server. By default, it will try to use HTTP/2 first and if it's not enabled, it will - * use HTTP/1.1. - * When the property `http2` is enabled, this flag will be automatically enabled. - */ - @ConfigItem - public Optional alpn; - - /** - * If {@code true}, the stacktrace of the invocation of the REST Client method is captured. - * This stacktrace will be used if the invocation throws an exception - */ - @ConfigItem - public Optional captureStacktrace; - - public static RestClientConfig load(String configKey) { - final RestClientConfig instance = new RestClientConfig(); - - instance.url = getConfigValue(configKey, "url", String.class); - instance.uri = getConfigValue(configKey, "uri", String.class); - instance.overrideUri = getConfigValue(configKey, "override-uri", String.class); - instance.providers = getConfigValue(configKey, "providers", String.class); - instance.connectTimeout = getConfigValue(configKey, "connect-timeout", Long.class); - instance.readTimeout = getConfigValue(configKey, "read-timeout", Long.class); - instance.followRedirects = getConfigValue(configKey, "follow-redirects", Boolean.class); - instance.multipartPostEncoderMode = getConfigValue(configKey, "multipart-post-encoder-mode", String.class); - instance.proxyAddress = getConfigValue(configKey, "proxy-address", String.class); - instance.proxyUser = getConfigValue(configKey, "proxy-user", String.class); - instance.proxyPassword = getConfigValue(configKey, "proxy-password", String.class); - instance.nonProxyHosts = getConfigValue(configKey, "non-proxy-hosts", String.class); - instance.queryParamStyle = getConfigValue(configKey, "query-param-style", QueryParamStyle.class); - instance.verifyHost = getConfigValue(configKey, "verify-host", Boolean.class); - instance.trustStore = getConfigValue(configKey, "trust-store", String.class); - instance.trustStorePassword = getConfigValue(configKey, "trust-store-password", String.class); - instance.trustStoreType = getConfigValue(configKey, "trust-store-type", String.class); - instance.keyStore = getConfigValue(configKey, "key-store", String.class); - instance.keyStorePassword = getConfigValue(configKey, "key-store-password", String.class); - instance.keyStoreType = getConfigValue(configKey, "key-store-type", String.class); - instance.hostnameVerifier = getConfigValue(configKey, "hostname-verifier", String.class); - instance.tlsConfigurationName = getConfigValue(configKey, "tls-configuration-name", String.class); - instance.connectionTTL = getConfigValue(configKey, "connection-ttl", Integer.class); - instance.connectionPoolSize = getConfigValue(configKey, "connection-pool-size", Integer.class); - instance.keepAliveEnabled = getConfigValue(configKey, "keep-alive-enabled", Boolean.class); - instance.maxRedirects = getConfigValue(configKey, "max-redirects", Integer.class); - instance.headers = getConfigValues(configKey, "headers", String.class, String.class); - instance.shared = getConfigValue(configKey, "shared", Boolean.class); - instance.name = getConfigValue(configKey, "name", String.class); - instance.userAgent = getConfigValue(configKey, "user-agent", String.class); - instance.http2 = getConfigValue(configKey, "http2", Boolean.class); - instance.maxChunkSize = getConfigValue(configKey, "max-chunk-size", MemorySize.class); - instance.alpn = getConfigValue(configKey, "alpn", Boolean.class); - instance.captureStacktrace = getConfigValue(configKey, "capture-stacktrace", Boolean.class); - - instance.multipart = new RestClientMultipartConfig(); - instance.multipart.maxChunkSize = getConfigValue(configKey, "multipart.max-chunk-size", Integer.class); - - return instance; - } - - public static RestClientConfig load(Class interfaceClass) { - final RestClientConfig instance = new RestClientConfig(); - - instance.url = getConfigValue(interfaceClass, "url", String.class); - instance.uri = getConfigValue(interfaceClass, "uri", String.class); - instance.overrideUri = getConfigValue(interfaceClass, "override-uri", String.class); - instance.providers = getConfigValue(interfaceClass, "providers", String.class); - instance.connectTimeout = getConfigValue(interfaceClass, "connect-timeout", Long.class); - instance.readTimeout = getConfigValue(interfaceClass, "read-timeout", Long.class); - instance.followRedirects = getConfigValue(interfaceClass, "follow-redirects", Boolean.class); - instance.proxyAddress = getConfigValue(interfaceClass, "proxy-address", String.class); - instance.proxyUser = getConfigValue(interfaceClass, "proxy-user", String.class); - instance.proxyPassword = getConfigValue(interfaceClass, "proxy-password", String.class); - instance.nonProxyHosts = getConfigValue(interfaceClass, "non-proxy-hosts", String.class); - instance.queryParamStyle = getConfigValue(interfaceClass, "query-param-style", QueryParamStyle.class); - instance.verifyHost = getConfigValue(interfaceClass, "verify-host", Boolean.class); - instance.trustStore = getConfigValue(interfaceClass, "trust-store", String.class); - instance.trustStorePassword = getConfigValue(interfaceClass, "trust-store-password", String.class); - instance.trustStoreType = getConfigValue(interfaceClass, "trust-store-type", String.class); - instance.keyStore = getConfigValue(interfaceClass, "key-store", String.class); - instance.keyStorePassword = getConfigValue(interfaceClass, "key-store-password", String.class); - instance.keyStoreType = getConfigValue(interfaceClass, "key-store-type", String.class); - instance.hostnameVerifier = getConfigValue(interfaceClass, "hostname-verifier", String.class); - instance.tlsConfigurationName = getConfigValue(interfaceClass, "tls-configuration-name", String.class); - instance.connectionTTL = getConfigValue(interfaceClass, "connection-ttl", Integer.class); - instance.connectionPoolSize = getConfigValue(interfaceClass, "connection-pool-size", Integer.class); - instance.keepAliveEnabled = getConfigValue(interfaceClass, "keep-alive-enabled", Boolean.class); - instance.maxRedirects = getConfigValue(interfaceClass, "max-redirects", Integer.class); - instance.headers = getConfigValues(interfaceClass, "headers", String.class, String.class); - instance.shared = getConfigValue(interfaceClass, "shared", Boolean.class); - instance.name = getConfigValue(interfaceClass, "name", String.class); - instance.userAgent = getConfigValue(interfaceClass, "user-agent", String.class); - instance.http2 = getConfigValue(interfaceClass, "http2", Boolean.class); - instance.maxChunkSize = getConfigValue(interfaceClass, "max-chunk-size", MemorySize.class); - instance.alpn = getConfigValue(interfaceClass, "alpn", Boolean.class); - instance.captureStacktrace = getConfigValue(interfaceClass, "capture-stacktrace", Boolean.class); - - instance.multipart = new RestClientMultipartConfig(); - instance.multipart.maxChunkSize = getConfigValue(interfaceClass, "multipart.max-chunk-size", Integer.class); - - return instance; - } - - public static Optional getConfigValue(String configKey, String fieldName, Class type) { - final Config config = ConfigProvider.getConfig(); - Optional optional = config.getOptionalValue(composePropertyKey(configKey, fieldName), type); - if (optional.isEmpty()) { // try to find property with quoted configKey - optional = config.getOptionalValue(composePropertyKey('"' + configKey + '"', fieldName), type); - } - return optional; - } - - public static Optional getConfigValue(Class clientInterface, String fieldName, Class type) { - final Config config = ConfigProvider.getConfig(); - // first try interface full name - Optional optional = config.getOptionalValue(composePropertyKey('"' + clientInterface.getName() + '"', fieldName), - type); - if (optional.isEmpty()) { // then interface simple name - optional = config.getOptionalValue(composePropertyKey(clientInterface.getSimpleName(), fieldName), type); - } - if (optional.isEmpty()) { // lastly quoted interface simple name - optional = config.getOptionalValue(composePropertyKey('"' + clientInterface.getSimpleName() + '"', fieldName), - type); - } - return optional; - } - - private static Map getConfigValues(String configKey, String fieldName, Class keyType, Class valueType) { - final SmallRyeConfig config = (SmallRyeConfig) ConfigProvider.getConfig(); - Optional> optional = config.getOptionalValues(composePropertyKey(configKey, fieldName), keyType, valueType); - if (optional.isEmpty()) { // try to find property with quoted configKey - optional = config.getOptionalValues(composePropertyKey('"' + configKey + '"', fieldName), keyType, valueType); - } - return optional.isPresent() ? optional.get() : Collections.emptyMap(); - } - - private static Map getConfigValues(Class clientInterface, String fieldName, Class keyType, - Class valueType) { - final SmallRyeConfig config = (SmallRyeConfig) ConfigProvider.getConfig(); - // first try interface full name - Optional> optional = config.getOptionalValues( - composePropertyKey('"' + clientInterface.getName() + '"', fieldName), - keyType, valueType); - if (optional.isEmpty()) { // then interface simple name - optional = config.getOptionalValues(composePropertyKey(clientInterface.getSimpleName(), fieldName), keyType, - valueType); - } - if (optional.isEmpty()) { // lastly quoted interface simple name - optional = config.getOptionalValues(composePropertyKey('"' + clientInterface.getSimpleName() + '"', fieldName), - keyType, valueType); - } - return optional.isPresent() ? optional.get() : Collections.emptyMap(); - } - - private static String composePropertyKey(String key, String fieldName) { - return Constants.QUARKUS_CONFIG_PREFIX + key + "." + fieldName; - } -} diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java index 84507edb3cb366..45e317299c5d65 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java @@ -59,9 +59,9 @@ public RestClientFallbackConfigSourceInterceptor() { /** * If an MP-style property is detected (e.g. "prefix/mp-rest/url"), * we need to include the relevant Quarkus-style property name ("quarkus.rest-client.prefix.url") in the iteration. - * + *

* This is required so that the BuildTimeConfigurationReader is aware that it should create the configuration objects for - * REST clients ({@link RestClientConfig}). + * REST clients ({@link RestClientsConfig.RestClientConfig}). */ @Override public Iterator iterateNames(final ConfigSourceInterceptorContext context) { @@ -94,9 +94,9 @@ public Iterator iterateNames(final ConfigSourceInterceptorContext contex } /** - * Splits a property key into client prefix and property name. If given key doesn't contain a client prefix, null will be - * returned in the first array item. - * + * Splits a property key into client prefix and property name. If given key doesn't contain a client prefix, null + * will be returned in the first array item. + *

* Examples: *

  • `client-prefix.url` will return `String[] {"client-prefix", "url"}`
  • *
  • `"client.prefix".url` will return `String[] {"client.prefix", "url"}`
  • diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientLoggingConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientLoggingConfig.java deleted file mode 100644 index fd4887126fb475..00000000000000 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientLoggingConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.quarkus.restclient.config; - -import java.util.Optional; - -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; - -@ConfigGroup -public class RestClientLoggingConfig { - /** - * Scope of logging for the client. - *
    - * WARNING: beware of logging sensitive data - *
    - * The possible values are: - *
      - *
    • {@code request-response} - enables logging request and responses, including redirect responses
    • - *
    • {@code all} - enables logging requests and responses and lower-level logging
    • - *
    • {@code none} - no additional logging
    • - *
    - * - * This property is applicable to reactive REST clients only. - */ - @ConfigItem - public Optional scope; - - /** - * How many characters of the body should be logged. Message body can be large and can easily pollute the logs. - *

    - * By default, set to 100. - *

    - * This property is applicable to reactive REST clients only. - */ - @ConfigItem(defaultValue = "100") - public Integer bodyLimit; -} diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientMultipartConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientMultipartConfig.java deleted file mode 100644 index 61e7932d7e704e..00000000000000 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientMultipartConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.quarkus.restclient.config; - -import java.util.Optional; - -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; - -@ConfigGroup -public class RestClientMultipartConfig { - - /** - * The max HTTP chunk size (8096 bytes by default). - *

    - * This property is applicable to reactive REST clients only. - * - * @Deprecated Use {@code quarkus.rest-client.max-chunk-size} instead - */ - @Deprecated - @ConfigItem - public Optional maxChunkSize; - -} diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientNameFallbackConfigSourceInterceptor.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientNameFallbackConfigSourceInterceptor.java new file mode 100644 index 00000000000000..7ca19d8dbea6b2 --- /dev/null +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientNameFallbackConfigSourceInterceptor.java @@ -0,0 +1,87 @@ +package io.quarkus.restclient.config; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import io.smallrye.config.ConfigMappingLoader; +import io.smallrye.config.ConfigMappingObject; +import io.smallrye.config.FallbackConfigSourceInterceptor; + +/** + * Fallbacks REST Client FQN to Simple Name. + *

    + * Ideally, this shouldn't be required. The old custom implementation allowed us to mix both names and merge them in a + * final configuration to use in the REST Client. The standard Config system does not support such a feature. If a + * configuration supports multiple names, the user has to use the same name across all configuration sources. No other + * Quarkus extension behaves this way because only the REST Client extension provides the custom code to make it work. + */ +public class RestClientNameFallbackConfigSourceInterceptor extends FallbackConfigSourceInterceptor { + public RestClientNameFallbackConfigSourceInterceptor(final List restClients) { + super(fallback(restClients)); + } + + private static Function fallback(final List restClients) { + Class implementationClass = ConfigMappingLoader + .getImplementationClass(RestClientsConfig.class); + Set ignoreNames = configMappingNames(implementationClass).get(RestClientsConfig.class.getName()).get("") + .stream() + .filter(s -> s.charAt(0) != '*') + .map(s -> "quarkus.rest-client." + s) + .collect(Collectors.toSet()); + + return new Function() { + @Override + public String apply(final String name) { + if (name.startsWith("quarkus.rest-client.")) { + if (ignoreNames.contains(name)) { + return name; + } + + for (RegisteredRestClient restClient : restClients) { + if (name.length() > 20 && name.charAt(20) == '"') { + String interfaceName = restClient.getFullName(); + if (name.regionMatches(21, interfaceName, 0, interfaceName.length())) { + if (name.length() > 21 + interfaceName.length() + && name.charAt(21 + interfaceName.length()) == '"') { + return "quarkus.rest-client." + restClient.getSimpleName() + + name.substring(21 + interfaceName.length() + 1); + } + } + } + } + } + return name; + } + }; + } + + /** + * Expose this as a public API in SmallRye Config + */ + @SuppressWarnings("unchecked") + @Deprecated(forRemoval = true) + private static Map>> configMappingNames(final Class implementationClass) { + try { + Method getNames = implementationClass.getDeclaredMethod("getNames"); + return (Map>>) getNames.invoke(null); + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } catch (IllegalAccessException e) { + throw new IllegalAccessError(e.getMessage()); + } catch (InvocationTargetException e) { + try { + throw e.getCause(); + } catch (RuntimeException | Error e2) { + throw e2; + } catch (Throwable t) { + throw new UndeclaredThrowableException(t); + } + } + } +} diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java index 16ac5b0776c4e8..0d7c3c1f587078 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsBuildTimeConfig.java @@ -1,18 +1,73 @@ package io.quarkus.restclient.config; -import java.util.Collections; import java.util.Map; +import java.util.Optional; -import io.quarkus.runtime.annotations.ConfigItem; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.runtime.annotations.ConfigDocIgnore; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithDefaults; +import io.smallrye.config.WithParentName; -@ConfigRoot(name = "rest-client", phase = ConfigPhase.BUILD_TIME) -public class RestClientsBuildTimeConfig { - +@ConfigMapping(prefix = "quarkus.rest-client") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface RestClientsBuildTimeConfig { /** * Configurations of REST client instances. */ - @ConfigItem(name = ConfigItem.PARENT) - public Map configs = Collections.emptyMap(); + @WithParentName + @WithDefaults + Map clients(); + + interface RestClientBuildConfig { + + /** + * The CDI scope to use for injection. This property can contain either a fully qualified class name of a CDI scope + * annotation (such as "jakarta.enterprise.context.ApplicationScoped") or its simple name (such as + * "ApplicationScoped"). + * By default, this is not set which means the interface is not registered as a bean unless it is annotated with + * {@link RegisterRestClient}. + * If an interface is not annotated with {@link RegisterRestClient} and this property is set, then Quarkus will make the + * interface + * a bean of the configured scope. + */ + Optional scope(); + + /** + * If set to true, then Quarkus will ensure that all calls from the REST client go through a local proxy + * server (that is managed by Quarkus). + * This can be very useful for capturing network traffic to a service that uses HTTPS. + *

    + * This property is not applicable to the RESTEasy Client, only the Quarkus REST client (formerly RESTEasy Reactive + * client). + *

    + * This property only applicable to dev and test mode. + */ + @WithDefault("false") + boolean enableLocalProxy(); + + /** + * This setting is used to select which proxy provider to use if there are multiple ones. + * It only applies if {@code enable-local-proxy} is true. + *

    + * The algorithm for picking between multiple provider is the following: + *

      + *
    • If only the default is around, use it (its name is {@code default})
    • + *
    • If there is only one besides the default, use it
    • + *
    • If there are multiple ones, fail
    • + *
    + */ + Optional localProxyProvider(); + + /** + * Collects unmapped properties in the REST Client namespace, including available runtime properties. + */ + @WithParentName + @ConfigDocIgnore + Map properties(); + } } diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java index ab20ac8605a6bf..32695f92e8fbac 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java @@ -2,25 +2,23 @@ import java.util.Map; import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import jakarta.enterprise.inject.CreationException; import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; -import io.quarkus.arc.Arc; -import io.quarkus.arc.InstanceHandle; import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigDocIgnore; import io.quarkus.runtime.annotations.ConfigDocMapKey; -import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.quarkus.runtime.configuration.MemorySize; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithDefaults; +import io.smallrye.config.WithParentName; -@ConfigRoot(name = "rest-client", phase = ConfigPhase.RUN_TIME) -public class RestClientsConfig { - +@ConfigMapping(prefix = "quarkus.rest-client") +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public interface RestClientsConfig { /** * Configurations of REST client instances. *

    @@ -28,16 +26,10 @@ public class RestClientsConfig { * a class bearing that annotation, in which case it is possible to use the short name, as well as fully qualified * name. */ - // This variable is only here to avoid warnings about unrecognized configuration keys. The map is otherwise ignored, - // and the RestClientConfig instances are loaded via `RestClientConfig#load()` methods instead. - @ConfigItem(name = ConfigItem.PARENT) - Map configKey; - - @SuppressWarnings("DeprecatedIsStillUsed") - // The @Deprecated annotation prevents this field from being included in generated docs. We only want the `configKey` field - // above to be included. - @Deprecated - private final Map configs = new ConcurrentHashMap<>(); + @WithParentName + @WithDefaults + @ConfigDocMapKey("client") + Map clients(); /** * Mode in which the form data are encoded. Possible values are `HTML5`, `RFC1738` and `RFC3986`. @@ -49,8 +41,7 @@ public class RestClientsConfig { *

    * This property is not applicable to the RESTEasy Client. */ - @ConfigItem - public Optional multipartPostEncoderMode; + Optional multipartPostEncoderMode(); /** * A string value in the form of `:` that specifies the HTTP proxy server hostname @@ -58,8 +49,7 @@ public class RestClientsConfig { *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional proxyAddress; + Optional proxyAddress(); /** * Proxy username, equivalent to the http.proxy or https.proxy JVM settings. @@ -68,8 +58,7 @@ public class RestClientsConfig { *

    * This property is not applicable to the RESTEasy Client. */ - @ConfigItem - public Optional proxyUser; + Optional proxyUser(); /** * Proxy password, equivalent to the http.proxyPassword or https.proxyPassword JVM settings. @@ -78,8 +67,7 @@ public class RestClientsConfig { *

    * This property is not applicable to the RESTEasy Client. */ - @ConfigItem - public Optional proxyPassword; + Optional proxyPassword(); /** * Hosts to access without proxy, similar to the http.nonProxyHosts or https.nonProxyHosts JVM settings. @@ -89,28 +77,23 @@ public class RestClientsConfig { *

    * This property is not applicable to the RESTEasy Client. */ - @ConfigItem - public Optional nonProxyHosts; - - public RestClientLoggingConfig logging; - - public RestClientMultipartConfig multipart; + Optional nonProxyHosts(); /** * A timeout in milliseconds that REST clients should wait to connect to the remote endpoint. *

    * Can be overwritten by client-specific settings. */ - @ConfigItem(defaultValue = "15000", defaultValueDocumentation = "15000 ms") - public Long connectTimeout; + @WithDefault("15000") + Long connectTimeout(); /** * A timeout in milliseconds that REST clients should wait for a response from the remote endpoint. *

    * Can be overwritten by client-specific settings. */ - @ConfigItem(defaultValue = "30000", defaultValueDocumentation = "30000 ms") - public Long readTimeout; + @WithDefault("30000") + Long readTimeout(); /** * If true, the REST clients will not provide additional contextual information (like REST client class and method @@ -118,8 +101,8 @@ public class RestClientsConfig { *

    * This property is not applicable to the RESTEasy Client. */ - @ConfigItem(defaultValue = "false") - public boolean disableContextualErrorMessages; + @WithDefault("false") + boolean disableContextualErrorMessages(); /** * Default configuration for the HTTP user-agent header to use in all REST clients. @@ -128,23 +111,20 @@ public class RestClientsConfig { *

    * This property is not applicable to the RESTEasy Client. */ - @ConfigItem - public Optional userAgent; + Optional userAgent(); /** * The HTTP headers that should be applied to all requests of the rest client. */ - @ConfigItem @ConfigDocMapKey("header-name") - public Map headers; + Map headers(); /** * The class name of the host name verifier. The class must have a public no-argument constructor. *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional hostnameVerifier; + Optional hostnameVerifier(); /** * The time in ms for which a connection remains unused in the connection pool before being evicted and closed. @@ -152,24 +132,22 @@ public class RestClientsConfig { *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional connectionTTL; + Optional connectionTTL(); /** * The size of the connection pool for this client. *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional connectionPoolSize; + Optional connectionPoolSize(); /** * If set to false disables the keep alive completely. *

    * Can be overwritten by client-specific settings. */ - @ConfigItem(defaultValue = "true") - public Optional keepAliveEnabled; + @WithDefault("true") + Optional keepAliveEnabled(); /** * The maximum number of redirection a request can follow. @@ -178,16 +156,14 @@ public class RestClientsConfig { *

    * This property is not applicable to the RESTEasy Client. */ - @ConfigItem - public Optional maxRedirects; + Optional maxRedirects(); /** * A boolean value used to determine whether the client should follow HTTP redirect responses. *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional followRedirects; + Optional followRedirects(); /** * Map where keys are fully-qualified provider classnames to include in the client, and values are their integer @@ -195,8 +171,7 @@ public class RestClientsConfig { *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional providers; + Optional providers(); /** * The CDI scope to use for injections of REST client instances. Value can be either a fully qualified class name of a CDI @@ -209,8 +184,7 @@ public class RestClientsConfig { *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional scope; + Optional scope(); /** * An enumerated type string value with possible values of "MULTI_PAIRS" (default), "COMMA_SEPARATED", @@ -218,8 +192,7 @@ public class RestClientsConfig { *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional queryParamStyle; + Optional queryParamStyle(); /** * Set whether hostname verification is enabled. Default is enabled. @@ -227,56 +200,49 @@ public class RestClientsConfig { *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional verifyHost; + Optional verifyHost(); /** * The trust store location. Can point to either a classpath resource or a file. *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional trustStore; + Optional trustStore(); /** * The trust store password. *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional trustStorePassword; + Optional trustStorePassword(); /** * The type of the trust store. Defaults to "JKS". *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional trustStoreType; + Optional trustStoreType(); /** * The key store location. Can point to either a classpath resource or a file. *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional keyStore; + Optional keyStore(); /** * The key store password. *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional keyStorePassword; + Optional keyStorePassword(); /** * The type of the key store. Defaults to "JKS". *

    * Can be overwritten by client-specific settings. */ - @ConfigItem - public Optional keyStoreType; + Optional keyStoreType(); /** * The name of the TLS configuration to use. @@ -289,23 +255,21 @@ public class RestClientsConfig { *

    * This property is not applicable to the RESTEasy Client. */ - @ConfigItem - public Optional tlsConfigurationName; + Optional tlsConfigurationName(); /** * If this is true then HTTP/2 will be enabled. */ - @ConfigItem(defaultValue = "false") - public boolean http2; + @WithDefault("false") + boolean http2(); /** * The max HTTP chunk size (8096 bytes by default). *

    * Can be overwritten by client-specific settings. */ - @ConfigItem @ConfigDocDefault("8k") - public Optional maxChunkSize; + Optional maxChunkSize(); /** * If the Application-Layer Protocol Negotiation is enabled, the client will negotiate which protocol to use over the @@ -313,54 +277,317 @@ public class RestClientsConfig { * use HTTP/1.1. * When the property `http2` is enabled, this flag will be automatically enabled. */ - @ConfigItem - public Optional alpn; + Optional alpn(); /** * If {@code true}, the stacktrace of the invocation of the REST Client method is captured. * This stacktrace will be used if the invocation throws an exception */ - @ConfigItem(defaultValue = "true") - public boolean captureStacktrace; + @WithDefault("true") + boolean captureStacktrace(); - public RestClientConfig getClientConfig(String configKey) { - if (configKey == null) { - return RestClientConfig.EMPTY; - } - return configs.computeIfAbsent(configKey, RestClientConfig::load); - } + /** + * Logging configuration. + */ + RestClientLoggingConfig logging(); + + /** + * Multipart configuration. + */ + RestClientMultipartConfig multipart(); - public RestClientConfig getClientConfig(Class clientInterface) { - return configs.computeIfAbsent(clientInterface.getName(), name -> RestClientConfig.load(clientInterface)); + default RestClientConfig getClient(final Class restClientInterface) { + // Check if the key is there first or else we will get the defaults from @WithDefaults + if (clients().containsKey(restClientInterface.getName())) { + return clients().get(restClientInterface.getName()); + } + return clients().get(restClientInterface.getSimpleName()); } - public void putClientConfig(String configKey, RestClientConfig clientConfig) { - configs.put(configKey, clientConfig); + default RestClientConfig getClient(final String restClientConfigKey) { + return clients().get(restClientConfigKey); } - public void putClientConfig(Class clientInterface, RestClientConfig clientConfig) { - configs.put(clientInterface.getName(), clientConfig); + interface RestClientLoggingConfig { + /** + * Scope of logging for the client. + *
    + * WARNING: beware of logging sensitive data + *
    + * The possible values are: + *

      + *
    • {@code request-response} - enables logging request and responses, including redirect responses
    • + *
    • {@code all} - enables logging requests and responses and lower-level logging
    • + *
    • {@code none} - no additional logging
    • + *
    + * + * This property is applicable to reactive REST clients only. + */ + Optional scope(); + + /** + * How many characters of the body should be logged. Message body can be large and can easily pollute the logs. + *

    + * By default, set to 100. + *

    + * This property is applicable to reactive REST clients only. + */ + @WithDefault("100") + Integer bodyLimit(); } - public Set getConfigKeys() { - return configs.keySet(); + interface RestClientMultipartConfig { + /** + * The max HTTP chunk size (8096 bytes by default). + *

    + * This property is applicable to reactive REST clients only. + * + * @deprecated Use {@code quarkus.rest-client.max-chunk-size} instead + */ + @Deprecated + Optional maxChunkSize(); } - public static RestClientsConfig getInstance() { - InstanceHandle configHandle; - try { - configHandle = Arc.container().instance(RestClientsConfig.class); - } catch (CreationException e) { - String message = "The Rest Client configuration cannot be initialized at this stage. " - + "Try to wrap your Rest Client injection in the Provider<> interface:\n\n" - + " @Inject\n" - + " @RestClient\n" - + " Provider myRestClient;\n"; - throw new RuntimeException(message, e); - } - if (!configHandle.isAvailable()) { - throw new IllegalStateException("Unable to find the RestClientConfigs"); - } - return configHandle.get(); + interface RestClientConfig { + /** + * Dummy configuration to force lookup of REST Client configurations. + * + * @see AbstractRestClientConfigBuilder + */ + @WithDefault("true") + @ConfigDocIgnore + boolean force(); + + /** + * Multipart configuration. + */ + RestClientMultipartConfig multipart(); + + /** + * The base URL to use for this service. This property or the `uri` property is considered required, unless + * the `baseUri` attribute is configured in the `@RegisterRestClient` annotation. + */ + Optional url(); + + /** + * The base URI to use for this service. This property or the `url` property is considered required, unless + * the `baseUri` attribute is configured in the `@RegisterRestClient` annotation. + */ + Optional uri(); + + /** + * This property is only meant to be set by advanced configurations to override whatever value was set for the uri or + * url. + * The override is done using the REST Client class name configuration syntax. + *

    + * This property is not applicable to the RESTEasy Client, only the Quarkus Rest client (formerly RESTEasy Reactive + * client). + */ + Optional overrideUri(); + + /** + * Map where keys are fully-qualified provider classnames to include in the client, and values are their integer + * priorities. The equivalent of the `@RegisterProvider` annotation. + */ + Optional providers(); + + /** + * Timeout specified in milliseconds to wait to connect to the remote endpoint. + */ + Optional connectTimeout(); + + /** + * Timeout specified in milliseconds to wait for a response from the remote endpoint. + */ + Optional readTimeout(); + + /** + * A boolean value used to determine whether the client should follow HTTP redirect responses. + */ + Optional followRedirects(); + + /** + * Mode in which the form data are encoded. Possible values are `HTML5`, `RFC1738` and `RFC3986`. + * The modes are described in the + * Netty + * documentation + *

    + * By default, Rest Client Reactive uses RFC1738. + *

    + * This property is not applicable to the RESTEasy Client. + */ + Optional multipartPostEncoderMode(); + + /** + * A string value in the form of `:` that specifies the HTTP proxy server hostname + * (or IP address) and port for requests of this client to use. + *

    + * Use `none` to disable proxy + */ + Optional proxyAddress(); + + /** + * Proxy username. + *

    + * This property is not applicable to the RESTEasy Client. + */ + Optional proxyUser(); + + /** + * Proxy password. + *

    + * This property is not applicable to the RESTEasy Client. + */ + Optional proxyPassword(); + + /** + * Hosts to access without proxy + *

    + * This property is not applicable to the RESTEasy Client. + */ + Optional nonProxyHosts(); + + /** + * An enumerated type string value with possible values of "MULTI_PAIRS" (default), "COMMA_SEPARATED", + * or "ARRAY_PAIRS" that specifies the format in which multiple values for the same query parameter is used. + */ + Optional queryParamStyle(); + + /** + * Set whether hostname verification is enabled. Default is enabled. + * This setting should not be disabled in production as it makes the client vulnerable to MITM attacks. + */ + Optional verifyHost(); + + /** + * The trust store location. Can point to either a classpath resource or a file. + */ + Optional trustStore(); + + /** + * The trust store password. + */ + Optional trustStorePassword(); + + /** + * The type of the trust store. Defaults to "JKS". + */ + Optional trustStoreType(); + + /** + * The key store location. Can point to either a classpath resource or a file. + */ + Optional keyStore(); + + /** + * The key store password. + */ + Optional keyStorePassword(); + + /** + * The type of the key store. Defaults to "JKS". + */ + Optional keyStoreType(); + + /** + * The class name of the host name verifier. The class must have a public no-argument constructor. + */ + Optional hostnameVerifier(); + + /** + * The name of the TLS configuration to use. + *

    + * If not set and the default TLS configuration is configured ({@code quarkus.tls.*}) then that will be used. + * If a name is configured, it uses the configuration from {@code quarkus.tls..*} + * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. + *

    + * If no TLS configuration is set, then the keys-tore, trust-store, etc. properties will be used. + *

    + * This property is not applicable to the RESTEasy Client. + */ + Optional tlsConfigurationName(); + + /** + * The time in ms for which a connection remains unused in the connection pool before being evicted and closed. + * A timeout of {@code 0} means there is no timeout. + */ + Optional connectionTTL(); + + /** + * The size of the connection pool for this client. + */ + Optional connectionPoolSize(); + + /** + * If set to false disables the keep alive completely. + */ + Optional keepAliveEnabled(); + + /** + * The maximum number of redirection a request can follow. + *

    + * This property is not applicable to the RESTEasy Client. + */ + Optional maxRedirects(); + + /** + * The HTTP headers that should be applied to all requests of the rest client. + *

    + * This property is not applicable to the RESTEasy Client. + */ + @ConfigDocMapKey("header-name") + Map headers(); + + /** + * Set to true to share the HTTP client between REST clients. + * There can be multiple shared clients distinguished by name, when no specific name is set, + * the name __vertx.DEFAULT is used. + *

    + * This property is not applicable to the RESTEasy Client. + */ + Optional shared(); + + /** + * Set the HTTP client name, used when the client is shared, otherwise ignored. + *

    + * This property is not applicable to the RESTEasy Client. + */ + Optional name(); + + /** + * Configure the HTTP user-agent header to use. + *

    + * This property is not applicable to the RESTEasy Client. + */ + Optional userAgent(); + + /** + * If this is true then HTTP/2 will be enabled. + */ + Optional http2(); + + /** + * The max HTTP ch + * unk size (8096 bytes by default). + *

    + * This property is not applicable to the RESTEasy Client. + */ + @ConfigDocDefault("8K") + Optional maxChunkSize(); + + /** + * If the Application-Layer Protocol Negotiation is enabled, the client will negotiate which protocol to use over the + * protocols exposed by the server. By default, it will try to use HTTP/2 first and if it's not enabled, it will + * use HTTP/1.1. + * When the property `http2` is enabled, this flag will be automatically enabled. + */ + Optional alpn(); + + /** + * If {@code true}, the stacktrace of the invocation of the REST Client method is captured. + * This stacktrace will be used if the invocation throws an exception + */ + Optional captureStacktrace(); } } diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.Converter b/extensions/resteasy-classic/rest-client-config/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.Converter deleted file mode 100644 index cd5453d3541f87..00000000000000 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.Converter +++ /dev/null @@ -1 +0,0 @@ -io.quarkus.restclient.config.QueryParamStyleConverter \ No newline at end of file diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java b/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java index 165cab90808e0b..b15cfcb84cfa00 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java @@ -1,65 +1,65 @@ package io.quarkus.restclient.config; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.math.BigInteger; -import java.net.URL; +import java.util.List; import java.util.Optional; import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.junit.jupiter.api.Test; -import io.smallrye.config.PropertiesConfigSource; +import io.quarkus.runtime.configuration.ConfigUtils; import io.smallrye.config.SmallRyeConfig; -import io.smallrye.config.SmallRyeConfigBuilder; public class RestClientConfigTest { @Test - public void testLoadRestClientConfig() throws IOException { - SmallRyeConfig config = createMPConfig(); + public void testLoadRestClientConfig() { + SmallRyeConfig config = ConfigUtils.emptyConfigBuilder() + .withMapping(RestClientsConfig.class) + .withInterceptors(new RestClientNameFallbackConfigSourceInterceptor( + List.of(new RegisteredRestClient(RestClientConfigTest.class.getName(), + RestClientConfigTest.class.getSimpleName())))) + .build(); Optional optionalValue = config.getOptionalValue("quarkus.rest-client.test-client.url", String.class); assertThat(optionalValue).isPresent(); - RestClientConfig configForKey = RestClientConfig.load("test-client"); + RestClientsConfig restClientsConfig = config.getConfigMapping(RestClientsConfig.class); + + RestClientsConfig.RestClientConfig configForKey = restClientsConfig.getClient("test-client"); verifyConfig(configForKey); - RestClientConfig configForClassName = RestClientConfig.load(RestClientConfigTest.class); - verifyConfig(configForClassName); - } - private void verifyConfig(RestClientConfig config) { - assertThat(config.url).isPresent(); - assertThat(config.url.get()).isEqualTo("http://localhost:8080"); - assertThat(config.uri).isPresent(); - assertThat(config.uri.get()).isEqualTo("http://localhost:8081"); - assertThat(config.providers).isPresent(); - assertThat(config.providers.get()).isEqualTo("io.quarkus.restclient.configuration.MyResponseFilter"); - assertThat(config.connectTimeout).isPresent(); - assertThat(config.connectTimeout.get()).isEqualTo(5000); - assertThat(config.readTimeout).isPresent(); - assertThat(config.readTimeout.get()).isEqualTo(6000); - assertThat(config.followRedirects).isPresent(); - assertThat(config.followRedirects.get()).isEqualTo(true); - assertThat(config.proxyAddress).isPresent(); - assertThat(config.proxyAddress.get()).isEqualTo("localhost:8080"); - assertThat(config.queryParamStyle).isPresent(); - assertThat(config.queryParamStyle.get()).isEqualTo(QueryParamStyle.COMMA_SEPARATED); - assertThat(config.hostnameVerifier).isPresent(); - assertThat(config.hostnameVerifier.get()).isEqualTo("io.quarkus.restclient.configuration.MyHostnameVerifier"); - assertThat(config.connectionTTL).isPresent(); - assertThat(config.connectionTTL.get()).isEqualTo(30000); - assertThat(config.connectionPoolSize).isPresent(); - assertThat(config.connectionPoolSize.get()).isEqualTo(10); - assertThat(config.maxChunkSize.get().asBigInteger()).isEqualTo(BigInteger.valueOf(1024)); + RestClientsConfig.RestClientConfig configForClassName = restClientsConfig.getClient(RestClientConfigTest.class); + verifyConfig(configForClassName); } - private static SmallRyeConfig createMPConfig() throws IOException { - SmallRyeConfigBuilder configBuilder = new SmallRyeConfigBuilder().addDefaultInterceptors(); - URL propertyFile = RestClientConfigTest.class.getClassLoader().getResource("application.properties"); - assertThat(propertyFile).isNotNull(); - configBuilder.withSources(new PropertiesConfigSource(propertyFile)); - return configBuilder.build(); + private void verifyConfig(RestClientsConfig.RestClientConfig config) { + assertTrue(config.url().isPresent()); + assertThat(config.url().get()).isEqualTo("http://localhost:8080"); + assertTrue(config.uri().isPresent()); + assertThat(config.uri().get()).isEqualTo("http://localhost:8081"); + assertTrue(config.providers().isPresent()); + assertThat(config.providers().get()).isEqualTo("io.quarkus.restclient.configuration.MyResponseFilter"); + assertTrue(config.connectTimeout().isPresent()); + assertThat(config.connectTimeout().get()).isEqualTo(5000); + assertTrue(config.readTimeout().isPresent()); + assertThat(config.readTimeout().get()).isEqualTo(6000); + assertTrue(config.followRedirects().isPresent()); + assertThat(config.followRedirects().get()).isEqualTo(true); + assertTrue(config.proxyAddress().isPresent()); + assertThat(config.proxyAddress().get()).isEqualTo("localhost:8080"); + assertTrue(config.queryParamStyle().isPresent()); + assertThat(config.queryParamStyle().get()).isEqualTo(QueryParamStyle.COMMA_SEPARATED); + assertTrue(config.hostnameVerifier().isPresent()); + assertThat(config.hostnameVerifier().get()).isEqualTo("io.quarkus.restclient.configuration.MyHostnameVerifier"); + assertTrue(config.connectionTTL().isPresent()); + assertThat(config.connectionTTL().get()).isEqualTo(30000); + assertTrue(config.connectionPoolSize().isPresent()); + assertThat(config.connectionPoolSize().get()).isEqualTo(10); + assertTrue(config.maxChunkSize().isPresent()); + assertThat(config.maxChunkSize().get().asBigInteger()).isEqualTo(BigInteger.valueOf(1024)); } } diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties b/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties index 3806ef06855d39..9cab1d64f4b4a3 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties @@ -13,7 +13,8 @@ quarkus.rest-client.test-client.connection-pool-size=10 quarkus.rest-client.test-client.max-chunk-size=1024 quarkus.rest-client."io.quarkus.restclient.config.RestClientConfigTest".url=http://localhost:8080 -quarkus.rest-client."RestClientConfigTest".uri=http://localhost:8081 +quarkus.rest-client."io.quarkus.restclient.config.RestClientConfigTest=x +quarkus.rest-client.RestClientConfigTest.uri=http://localhost:8081 quarkus.rest-client.RestClientConfigTest.scope=Singleton quarkus.rest-client."io.quarkus.restclient.config.RestClientConfigTest".providers=io.quarkus.restclient.configuration.MyResponseFilter quarkus.rest-client."io.quarkus.restclient.config.RestClientConfigTest".connect-timeout=5000 diff --git a/extensions/resteasy-classic/resteasy-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java b/extensions/resteasy-classic/resteasy-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java index 0b450da41ae0fc..1d12d5479ccb59 100644 --- a/extensions/resteasy-classic/resteasy-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java +++ b/extensions/resteasy-classic/resteasy-client/deployment/src/main/java/io/quarkus/restclient/deployment/RestClientProcessor.java @@ -72,6 +72,9 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; +import io.quarkus.deployment.builditem.StaticInitConfigBuilderBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; @@ -82,6 +85,7 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.restclient.NoopHostnameVerifier; +import io.quarkus.restclient.config.RegisteredRestClient; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.restclient.config.deployment.RestClientConfigUtils; import io.quarkus.restclient.runtime.PathFeatureHandler; @@ -212,10 +216,6 @@ void processInterfaces( return; } - for (DotName interfaze : interfaces.keySet()) { - restClient.produce(new RestClientBuildItem(interfaze.toString())); - } - warnAboutNotWorkingFeaturesInNative(nativeConfig, interfaces); for (Map.Entry entry : interfaces.entrySet()) { @@ -249,19 +249,31 @@ void processInterfaces( for (Map.Entry entry : interfaces.entrySet()) { DotName restClientName = entry.getKey(); + ClassInfo classInfo = entry.getValue(); + + Optional configKey; + Optional baseUri; + AnnotationInstance instance = classInfo.declaredAnnotation(REGISTER_REST_CLIENT); + if (instance != null) { + AnnotationValue configKeyValue = instance.value("configKey"); + configKey = configKeyValue == null ? Optional.empty() : Optional.of(configKeyValue.asString()); + AnnotationValue baseUriValue = instance.value("baseUri"); + baseUri = baseUriValue == null ? Optional.empty() : Optional.of(baseUriValue.asString()); + } else { + configKey = Optional.empty(); + baseUri = Optional.empty(); + } + ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem.configure(restClientName); // The spec is not clear whether we should add superinterfaces too - let's keep aligned with SmallRye for now configurator.addType(restClientName); configurator.addQualifier(REST_CLIENT); - final Optional configKey = getConfigKey(entry.getValue()); - final ScopeInfo scope = computeDefaultScope(capabilities, config, entry, configKey); - final List clientProviders = checkRestClientProviders(entry.getValue(), - restClientProviders); - configurator.scope(scope); + List clientProviders = checkRestClientProviders(entry.getValue(), restClientProviders); + configurator.scope(computeDefaultScope(capabilities, config, entry, configKey)); configurator.creator(m -> { // return new RestClientBase(proxyType, baseUri).create(); ResultHandle interfaceHandle = m.loadClassFromTCCL(restClientName.toString()); - ResultHandle baseUriHandle = m.load(getAnnotationParameter(entry.getValue(), "baseUri")); + ResultHandle baseUriHandle = baseUri.isPresent() ? m.load(baseUri.get()) : m.loadNull(); ResultHandle configKeyHandle = configKey.isPresent() ? m.load(configKey.get()) : m.loadNull(); ResultHandle restClientProvidersHandle; if (!clientProviders.isEmpty()) { @@ -284,9 +296,28 @@ void processInterfaces( configurator.destroyer(BeanDestroyer.CloseableDestroyer.class); syntheticBeans.produce(configurator.done()); + restClient.produce(new RestClientBuildItem(classInfo, configKey, baseUri)); } } + @BuildStep + void generateRestClientConfigBuilder( + List restClients, + BuildProducer generatedClass, + BuildProducer staticInitConfigBuilder, + BuildProducer runTimeConfigBuilder) { + + List registeredRestClients = restClients.stream() + .map(rc -> new RegisteredRestClient( + rc.getClassInfo().name().toString(), + rc.getClassInfo().simpleName(), + rc.getConfigKey().orElse(null))) + .toList(); + + RestClientConfigUtils.generateRestClientConfigBuilder(registeredRestClients, generatedClass, staticInitConfigBuilder, + runTimeConfigBuilder); + } + @BuildStep void clientTracingFeature(Capabilities capabilities, Optional metricsCapability, BuildProducer producer) { @@ -387,14 +418,6 @@ private void processInterfaceReturnTypes(ClassInfo classInfo, Set returnTy } } - private Optional getConfigKey(ClassInfo classInfo) { - String configKey = getAnnotationParameter(classInfo, "configKey"); - if (configKey.isEmpty()) { - return Optional.empty(); - } - return Optional.of(configKey); - } - private ScopeInfo computeDefaultScope(Capabilities capabilities, Config config, Map.Entry entry, Optional configKey) { ScopeInfo scopeToUse = null; @@ -451,20 +474,6 @@ private ScopeInfo computeDefaultScope(Capabilities capabilities, Config config, return scopeToUse != null ? scopeToUse : globalDefaultScope.getInfo(); } - private String getAnnotationParameter(ClassInfo classInfo, String parameterName) { - AnnotationInstance instance = classInfo.declaredAnnotation(REGISTER_REST_CLIENT); - if (instance == null) { - return ""; - } - - AnnotationValue value = instance.value(parameterName); - if (value == null) { - return ""; - } - - return value.asString(); - } - @BuildStep IgnoreClientProviderBuildItem ignoreMPPublisher() { // hack to remove a provider that is manually registered QuarkusRestClientBuilder diff --git a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/GlobalConfigurationTest.java b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/GlobalConfigurationTest.java index 38687c778997e0..4b855ea061cda6 100644 --- a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/GlobalConfigurationTest.java +++ b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/GlobalConfigurationTest.java @@ -50,35 +50,34 @@ void shouldRespond() { @Test void checkGlobalConfigValues() { // global properties: - assertThat(configRoot.multipartPostEncoderMode.get()).isEqualTo("HTML5"); - assertThat(configRoot.disableContextualErrorMessages).isTrue(); + assertThat(configRoot.multipartPostEncoderMode().get()).isEqualTo("HTML5"); + assertThat(configRoot.disableContextualErrorMessages()).isTrue(); // global defaults for client specific properties: - assertThat(configRoot.scope.get()).isEqualTo("Singleton"); - assertThat(configRoot.proxyAddress.get()).isEqualTo("host:123"); - assertThat(configRoot.proxyUser.get()).isEqualTo("proxyUser"); - assertThat(configRoot.proxyPassword.get()).isEqualTo("proxyPassword"); - assertThat(configRoot.nonProxyHosts.get()).isEqualTo("nonProxyHosts"); - assertThat(configRoot.connectTimeout).isEqualTo(2000); - assertThat(configRoot.readTimeout).isEqualTo(2001); - assertThat(configRoot.userAgent.get()).isEqualTo("agent"); - assertThat(configRoot.headers).isEqualTo(Collections.singletonMap("foo", "bar")); - assertThat(configRoot.hostnameVerifier.get()) + assertThat(configRoot.scope().get()).isEqualTo("Singleton"); + assertThat(configRoot.proxyAddress().get()).isEqualTo("host:123"); + assertThat(configRoot.proxyUser().get()).isEqualTo("proxyUser"); + assertThat(configRoot.proxyPassword().get()).isEqualTo("proxyPassword"); + assertThat(configRoot.nonProxyHosts().get()).isEqualTo("nonProxyHosts"); + assertThat(configRoot.connectTimeout()).isEqualTo(2000); + assertThat(configRoot.readTimeout()).isEqualTo(2001); + assertThat(configRoot.userAgent().get()).isEqualTo("agent"); + assertThat(configRoot.headers()).isEqualTo(Collections.singletonMap("foo", "bar")); + assertThat(configRoot.hostnameVerifier().get()) .isEqualTo("io.quarkus.restclient.configuration.MyHostnameVerifier"); - assertThat(configRoot.connectionTTL.get()).isEqualTo(20000); // value in ms, will be converted to seconds - assertThat(configRoot.connectionPoolSize.get()).isEqualTo(2); - assertThat(configRoot.maxRedirects.get()).isEqualTo(2); - assertThat(configRoot.followRedirects.get()).isTrue(); - assertThat(configRoot.providers.get()) + assertThat(configRoot.connectionTTL().get()).isEqualTo(20000); // value in ms, will be converted to seconds + assertThat(configRoot.connectionPoolSize().get()).isEqualTo(2); + assertThat(configRoot.maxRedirects().get()).isEqualTo(2); + assertThat(configRoot.followRedirects().get()).isTrue(); + assertThat(configRoot.providers().get()) .isEqualTo("io.quarkus.restclient.configuration.MyResponseFilter"); - assertThat(configRoot.queryParamStyle.get()).isEqualTo(QueryParamStyle.MULTI_PAIRS); + assertThat(configRoot.queryParamStyle().get()).isEqualTo(QueryParamStyle.MULTI_PAIRS); - assertThat(configRoot.trustStore.get()).isEqualTo("/path"); - assertThat(configRoot.trustStorePassword.get()).isEqualTo("password"); - assertThat(configRoot.trustStoreType.get()).isEqualTo("JKS"); - assertThat(configRoot.keyStore.get()).isEqualTo("/path"); - assertThat(configRoot.keyStorePassword.get()).isEqualTo("password"); - assertThat(configRoot.keyStoreType.get()).isEqualTo("JKS"); + assertThat(configRoot.trustStore().get()).isEqualTo("/path"); + assertThat(configRoot.trustStorePassword().get()).isEqualTo("password"); + assertThat(configRoot.trustStoreType().get()).isEqualTo("JKS"); + assertThat(configRoot.keyStore().get()).isEqualTo("/path"); + assertThat(configRoot.keyStorePassword().get()).isEqualTo("password"); + assertThat(configRoot.keyStoreType().get()).isEqualTo("JKS"); } - } diff --git a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/QuarkusConfigurationTest.java b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/QuarkusConfigurationTest.java index a333943a45e003..4e9d72b3ee2db2 100644 --- a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/QuarkusConfigurationTest.java +++ b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/QuarkusConfigurationTest.java @@ -1,6 +1,7 @@ package io.quarkus.restclient.configuration; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Set; @@ -16,7 +17,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.arc.Arc; -import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.test.QuarkusUnitTest; @@ -45,38 +45,38 @@ void shouldHaveSingletonScope() { @Test void configurationsShouldBeLoaded() { - verifyClientConfig(configRoot.getClientConfig("echo-client"), true); - verifyClientConfig(configRoot.getClientConfig("io.quarkus.restclient.configuration.EchoClient"), true); - verifyClientConfig(configRoot.getClientConfig("EchoClient"), true); - verifyClientConfig(configRoot.getClientConfig("mp-client"), false); // non-standard properties cannot be set via MP style config - verifyClientConfig(configRoot.getClientConfig("a.b.c.Client"), false); + verifyClientConfig(configRoot.clients().get("echo-client"), true); + verifyClientConfig(configRoot.clients().get("io.quarkus.restclient.configuration.EchoClient"), true); + verifyClientConfig(configRoot.clients().get("EchoClient"), true); + verifyClientConfig(configRoot.clients().get("mp-client"), false); // non-standard properties cannot be set via MP style config + verifyClientConfig(configRoot.clients().get("a.b.c.Client"), false); } - void verifyClientConfig(RestClientConfig clientConfig, boolean verifyNonStandardProperties) { - assertThat(clientConfig.url).isPresent(); - assertThat(clientConfig.url.get()).contains("localhost"); - assertThat(clientConfig.providers).isPresent(); - assertThat(clientConfig.providers.get()) + void verifyClientConfig(RestClientsConfig.RestClientConfig clientConfig, boolean verifyNonStandardProperties) { + assertTrue(clientConfig.url().isPresent()); + assertThat(clientConfig.url().get()).contains("localhost"); + assertTrue(clientConfig.providers().isPresent()); + assertThat(clientConfig.providers().get()) .isEqualTo("io.quarkus.restclient.configuration.MyResponseFilter"); - assertThat(clientConfig.connectTimeout).isPresent(); - assertThat(clientConfig.connectTimeout.get()).isEqualTo(5000); - assertThat(clientConfig.readTimeout).isPresent(); - assertThat(clientConfig.readTimeout.get()).isEqualTo(6000); - assertThat(clientConfig.followRedirects).isPresent(); - assertThat(clientConfig.followRedirects.get()).isEqualTo(true); - assertThat(clientConfig.proxyAddress).isPresent(); - assertThat(clientConfig.proxyAddress.get()).isEqualTo("localhost:8080"); - assertThat(clientConfig.queryParamStyle).isPresent(); - assertThat(clientConfig.queryParamStyle.get()).isEqualTo(QueryParamStyle.COMMA_SEPARATED); - assertThat(clientConfig.hostnameVerifier).isPresent(); - assertThat(clientConfig.hostnameVerifier.get()) + assertTrue(clientConfig.connectTimeout().isPresent()); + assertThat(clientConfig.connectTimeout().get()).isEqualTo(5000); + assertTrue(clientConfig.readTimeout().isPresent()); + assertThat(clientConfig.readTimeout().get()).isEqualTo(6000); + assertTrue(clientConfig.followRedirects().isPresent()); + assertThat(clientConfig.followRedirects().get()).isEqualTo(true); + assertTrue(clientConfig.proxyAddress().isPresent()); + assertThat(clientConfig.proxyAddress().get()).isEqualTo("localhost:8080"); + assertTrue(clientConfig.queryParamStyle().isPresent()); + assertThat(clientConfig.queryParamStyle().get()).isEqualTo(QueryParamStyle.COMMA_SEPARATED); + assertTrue(clientConfig.hostnameVerifier().isPresent()); + assertThat(clientConfig.hostnameVerifier().get()) .isEqualTo("io.quarkus.restclient.configuration.MyHostnameVerifier"); if (verifyNonStandardProperties) { - assertThat(clientConfig.connectionTTL).isPresent(); - assertThat(clientConfig.connectionTTL.get()).isEqualTo(30000); - assertThat(clientConfig.connectionPoolSize).isPresent(); - assertThat(clientConfig.connectionPoolSize.get()).isEqualTo(10); + assertTrue(clientConfig.connectionTTL().isPresent()); + assertThat(clientConfig.connectionTTL().get()).isEqualTo(30000); + assertTrue(clientConfig.connectionPoolSize().isPresent()); + assertThat(clientConfig.connectionPoolSize().get()).isEqualTo(10); } } } diff --git a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/RestClientConfigNotationTest.java b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/RestClientConfigNotationTest.java index 98d30635cbae81..fba3b4fdf238d9 100644 --- a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/RestClientConfigNotationTest.java +++ b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/RestClientConfigNotationTest.java @@ -6,32 +6,19 @@ import java.util.Map; import java.util.Set; -import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.spi.ConfigSource; -import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.spec.JavaArchive; -import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import com.google.common.collect.Iterators; - -import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsConfig; -import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.common.AbstractConfigSource; public class RestClientConfigNotationTest { private static final String URL = "localhost:8080"; - @RegisterExtension - static final QuarkusUnitTest TEST = new QuarkusUnitTest().setArchiveProducer( - () -> ShrinkWrap.create(JavaArchive.class) - .addClasses(EchoClient.class, TestConfigSource.class) - .addAsServiceProvider("org.eclipse.microprofile.config.spi.ConfigSource", - "io.quarkus.restclient.configuration.RestClientConfigNotationTest$TestConfigSource")); - @ParameterizedTest @ValueSource(strings = { "quarkus.rest-client.\"io.quarkus.restclient.configuration.EchoClient\".url", @@ -41,9 +28,13 @@ public class RestClientConfigNotationTest { }) public void testInterfaceConfiguration(final String urlPropertyName) { TestConfigSource.urlPropertyName = urlPropertyName; + SmallRyeConfig config = ConfigUtils.emptyConfigBuilder() + .withMapping(RestClientsConfig.class) + .withSources(new TestConfigSource()) + .build(); - RestClientsConfig configRoot = new RestClientsConfig(); - RestClientConfig clientConfig = configRoot.getClientConfig(EchoClient.class); + RestClientsConfig configRoot = config.getConfigMapping(RestClientsConfig.class); + RestClientsConfig.RestClientConfig clientConfig = configRoot.getClient(EchoClient.class); verifyConfig(clientConfig, urlPropertyName); } @@ -55,20 +46,23 @@ public void testInterfaceConfiguration(final String urlPropertyName) { }) public void testConfigKeyConfiguration(final String urlPropertyName) { TestConfigSource.urlPropertyName = urlPropertyName; - RestClientsConfig configRoot = new RestClientsConfig(); - RestClientConfig clientConfig = configRoot.getClientConfig("echo-client"); + SmallRyeConfig config = ConfigUtils.emptyConfigBuilder() + .withMapping(RestClientsConfig.class) + .withSources(new TestConfigSource()) + .build(); + RestClientsConfig configRoot = config.getConfigMapping(RestClientsConfig.class); + RestClientsConfig.RestClientConfig clientConfig = configRoot.getClient("echo-client"); verifyConfig(clientConfig, urlPropertyName); } - private void verifyConfig(final RestClientConfig clientConfig, final String urlPropertyName) { - ConfigSource configSource = Iterators.find(ConfigProvider.getConfig().getConfigSources().iterator(), - c -> c.getName().equals(TestConfigSource.SOURCE_NAME)); + private void verifyConfig(final RestClientsConfig.RestClientConfig clientConfig, final String urlPropertyName) { + ConfigSource configSource = new TestConfigSource(); assertThat(configSource.getPropertyNames()).containsOnly(urlPropertyName); assertThat(configSource.getValue(urlPropertyName)).isEqualTo(URL); - assertThat(clientConfig.url).isPresent(); - assertThat(clientConfig.url.get()).isEqualTo(URL); + assertThat(clientConfig.url()).isPresent(); + assertThat(clientConfig.url().get()).isEqualTo(URL); } public static class TestConfigSource extends AbstractConfigSource { diff --git a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/RestClientOverrideRuntimeConfigTest.java b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/RestClientOverrideRuntimeConfigTest.java index f8e84e01aaf29e..b471a07e5d2af2 100644 --- a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/RestClientOverrideRuntimeConfigTest.java +++ b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/RestClientOverrideRuntimeConfigTest.java @@ -64,8 +64,8 @@ void overrideConfig() { // We use the Quarkus name, because that is the one that has priority assertEquals(quarkusValue.getName(), "quarkus.rest-client.\"io.quarkus.restclient.configuration.EchoClient\".url"); - assertTrue(restClientsConfig.getConfigKeys().contains("io.quarkus.restclient.configuration.EchoClient")); - Optional url = restClientsConfig.getClientConfig("io.quarkus.restclient.configuration.EchoClient").url; + assertTrue(restClientsConfig.clients().containsKey("io.quarkus.restclient.configuration.EchoClient")); + Optional url = restClientsConfig.clients().get("io.quarkus.restclient.configuration.EchoClient").url(); assertTrue(url.isPresent()); assertEquals(url.get(), mpValue.getValue()); assertEquals(url.get(), quarkusValue.getValue()); diff --git a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/UnknownConfigTest.java b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/UnknownConfigTest.java index 430b7e713a5ffe..e792fbafcb2517 100644 --- a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/UnknownConfigTest.java +++ b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/UnknownConfigTest.java @@ -12,7 +12,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.test.QuarkusUnitTest; @@ -31,14 +30,14 @@ public class UnknownConfigTest { @Test void testClientConfigsArePresent() { - verifyClientConfig(restClientsConfig.getClientConfig("echo-client")); - verifyClientConfig(restClientsConfig.getClientConfig("io.quarkus.restclient.configuration.EchoClient")); - verifyClientConfig(restClientsConfig.getClientConfig("EchoClient")); - verifyClientConfig(restClientsConfig.getClientConfig("a.b.c.Client")); + verifyClientConfig(restClientsConfig.clients().get("echo-client")); + verifyClientConfig(restClientsConfig.clients().get("io.quarkus.restclient.configuration.EchoClient")); + verifyClientConfig(restClientsConfig.clients().get("EchoClient")); + verifyClientConfig(restClientsConfig.clients().get("a.b.c.Client")); } - private void verifyClientConfig(RestClientConfig config) { - assertTrue(config.url.isPresent()); - assertEquals("http://localhost:8081", config.url.get()); + private void verifyClientConfig(RestClientsConfig.RestClientConfig config) { + assertTrue(config.url().isPresent()); + assertEquals("http://localhost:8081", config.url().get()); } } diff --git a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/VaultScenarioRestClientConfigTest.java b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/VaultScenarioRestClientConfigTest.java index 5a4e7ace7831b8..ba353a963c4685 100644 --- a/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/VaultScenarioRestClientConfigTest.java +++ b/extensions/resteasy-classic/resteasy-client/deployment/src/test/java/io/quarkus/restclient/configuration/VaultScenarioRestClientConfigTest.java @@ -21,7 +21,7 @@ /** * This test makes sure that the rest client configuration is loaded even if it's provided by a ConfigSource which doesn't * list its contents via {@link ConfigSource#getPropertyNames()} (e.g. {@link VaultLikeConfigSource}). - * + *

    * This wasn't working when the configuration was accessed through a ConfigSource map - rest client initialization would fail * because no URI/URL configuration was obtained. */ diff --git a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java index df5ccb883169e5..d399714f3b5abc 100644 --- a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java +++ b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/RestClientBase.java @@ -18,6 +18,7 @@ import javax.net.ssl.HostnameVerifier; +import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.context.ManagedExecutor; import org.eclipse.microprofile.rest.client.RestClientBuilder; import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; @@ -25,8 +26,8 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.InstanceHandle; import io.quarkus.restclient.NoopHostnameVerifier; -import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsConfig; +import io.smallrye.config.SmallRyeConfig; public class RestClientBase { @@ -43,7 +44,7 @@ public class RestClientBase { public RestClientBase(Class proxyType, String baseUriFromAnnotation, String configKey, Class[] clientProviders) { this(proxyType, baseUriFromAnnotation, configKey, clientProviders, - RestClientsConfig.getInstance()); + ConfigProvider.getConfig().unwrap(SmallRyeConfig.class).getConfigMapping(RestClientsConfig.class)); } RestClientBase(Class proxyType, String baseUriFromAnnotation, String configKey, @@ -79,14 +80,14 @@ protected void configureBuilder(RestClientBuilder builder) { } protected void configureCustomProperties(RestClientBuilder builder) { - Optional connectionPoolSize = oneOf(clientConfigByClassName().connectionPoolSize, - clientConfigByConfigKey().connectionPoolSize, configRoot.connectionPoolSize); + Optional connectionPoolSize = oneOf(clientConfigByClassName().connectionPoolSize(), + clientConfigByConfigKey().connectionPoolSize(), configRoot.connectionPoolSize()); if (connectionPoolSize.isPresent()) { builder.property("resteasy.connectionPoolSize", connectionPoolSize.get()); } - Optional connectionTTL = oneOf(clientConfigByClassName().connectionTTL, - clientConfigByConfigKey().connectionTTL, configRoot.connectionTTL); + Optional connectionTTL = oneOf(clientConfigByClassName().connectionTTL(), + clientConfigByConfigKey().connectionTTL(), configRoot.connectionTTL()); if (connectionTTL.isPresent()) { builder.property("resteasy.connectionTTL", Arrays.asList(connectionTTL.get(), TimeUnit.MILLISECONDS)); @@ -94,8 +95,9 @@ protected void configureCustomProperties(RestClientBuilder builder) { } protected void configureProxy(RestClientBuilder builder) { - Optional proxyAddress = oneOf(clientConfigByClassName().proxyAddress, clientConfigByConfigKey().proxyAddress, - configRoot.proxyAddress); + Optional proxyAddress = oneOf(clientConfigByClassName().proxyAddress(), + clientConfigByConfigKey().proxyAddress(), + configRoot.proxyAddress()); if (proxyAddress.isPresent() && !NONE.equals(proxyAddress.get())) { String proxyString = proxyAddress.get(); @@ -118,42 +120,42 @@ protected void configureProxy(RestClientBuilder builder) { } protected void configureRedirects(RestClientBuilder builder) { - Optional followRedirects = oneOf(clientConfigByClassName().followRedirects, - clientConfigByConfigKey().followRedirects, configRoot.followRedirects); + Optional followRedirects = oneOf(clientConfigByClassName().followRedirects(), + clientConfigByConfigKey().followRedirects(), configRoot.followRedirects()); if (followRedirects.isPresent()) { builder.followRedirects(followRedirects.get()); } } protected void configureQueryParamStyle(RestClientBuilder builder) { - Optional queryParamStyle = oneOf(clientConfigByClassName().queryParamStyle, - clientConfigByConfigKey().queryParamStyle, configRoot.queryParamStyle); + Optional queryParamStyle = oneOf(clientConfigByClassName().queryParamStyle(), + clientConfigByConfigKey().queryParamStyle(), configRoot.queryParamStyle()); if (queryParamStyle.isPresent()) { builder.queryParamStyle(queryParamStyle.get()); } } protected void configureSsl(RestClientBuilder builder) { - Optional trustStore = oneOf(clientConfigByClassName().trustStore, clientConfigByConfigKey().trustStore, - configRoot.trustStore); + Optional trustStore = oneOf(clientConfigByClassName().trustStore(), clientConfigByConfigKey().trustStore(), + configRoot.trustStore()); if (trustStore.isPresent() && !trustStore.get().isBlank() && !NONE.equals(trustStore.get())) { registerTrustStore(trustStore.get(), builder); } - Optional keyStore = oneOf(clientConfigByClassName().keyStore, clientConfigByConfigKey().keyStore, - configRoot.keyStore); + Optional keyStore = oneOf(clientConfigByClassName().keyStore(), clientConfigByConfigKey().keyStore(), + configRoot.keyStore()); if (keyStore.isPresent() && !keyStore.get().isBlank() && !NONE.equals(keyStore.get())) { registerKeyStore(keyStore.get(), builder); } - Optional hostnameVerifier = oneOf(clientConfigByClassName().hostnameVerifier, - clientConfigByConfigKey().hostnameVerifier, configRoot.hostnameVerifier); + Optional hostnameVerifier = oneOf(clientConfigByClassName().hostnameVerifier(), + clientConfigByConfigKey().hostnameVerifier(), configRoot.hostnameVerifier()); if (hostnameVerifier.isPresent()) { registerHostnameVerifier(hostnameVerifier.get(), builder); } else { // If `verify-host` is disabled, we configure the client using the `NoopHostnameVerifier` verifier. - Optional verifyHost = oneOf(clientConfigByClassName().verifyHost, clientConfigByConfigKey().verifyHost, - configRoot.verifyHost); + Optional verifyHost = oneOf(clientConfigByClassName().verifyHost(), clientConfigByConfigKey().verifyHost(), + configRoot.verifyHost()); if (verifyHost.isPresent() && !verifyHost.get()) { registerHostnameVerifier(NoopHostnameVerifier.class.getName(), builder); } @@ -182,12 +184,12 @@ private void registerHostnameVerifier(String verifier, RestClientBuilder builder private void registerKeyStore(String keyStorePath, RestClientBuilder builder) { try { - Optional keyStoreType = oneOf(clientConfigByClassName().keyStoreType, - clientConfigByConfigKey().keyStoreType, configRoot.keyStoreType); + Optional keyStoreType = oneOf(clientConfigByClassName().keyStoreType(), + clientConfigByConfigKey().keyStoreType(), configRoot.keyStoreType()); KeyStore keyStore = KeyStore.getInstance(keyStoreType.orElse("JKS")); - Optional keyStorePassword = oneOf(clientConfigByClassName().keyStorePassword, - clientConfigByConfigKey().keyStorePassword, configRoot.keyStorePassword); + Optional keyStorePassword = oneOf(clientConfigByClassName().keyStorePassword(), + clientConfigByConfigKey().keyStorePassword(), configRoot.keyStorePassword()); if (keyStorePassword.isEmpty()) { throw new IllegalArgumentException("No password provided for keystore"); } @@ -208,12 +210,12 @@ private void registerKeyStore(String keyStorePath, RestClientBuilder builder) { private void registerTrustStore(String trustStorePath, RestClientBuilder builder) { try { - Optional trustStoreType = oneOf(clientConfigByClassName().trustStoreType, - clientConfigByConfigKey().trustStoreType, configRoot.trustStoreType); + Optional trustStoreType = oneOf(clientConfigByClassName().trustStoreType(), + clientConfigByConfigKey().trustStoreType(), configRoot.trustStoreType()); KeyStore trustStore = KeyStore.getInstance(trustStoreType.orElse("JKS")); - Optional trustStorePassword = oneOf(clientConfigByClassName().trustStorePassword, - clientConfigByConfigKey().trustStorePassword, configRoot.trustStorePassword); + Optional trustStorePassword = oneOf(clientConfigByClassName().trustStorePassword(), + clientConfigByConfigKey().trustStorePassword(), configRoot.trustStorePassword()); if (trustStorePassword.isEmpty()) { throw new IllegalArgumentException("No password provided for truststore"); } @@ -258,8 +260,8 @@ private InputStream locateStream(String path) throws FileNotFoundException { } protected void configureProviders(RestClientBuilder builder) { - Optional providers = oneOf(clientConfigByClassName().providers, clientConfigByConfigKey().providers, - configRoot.providers); + Optional providers = oneOf(clientConfigByClassName().providers(), clientConfigByConfigKey().providers(), + configRoot.providers()); if (providers.isPresent()) { registerProviders(builder, providers.get()); @@ -286,23 +288,23 @@ private Class providerClassForName(String name) { } protected void configureTimeouts(RestClientBuilder builder) { - Long connectTimeout = oneOf(clientConfigByClassName().connectTimeout, - clientConfigByConfigKey().connectTimeout).orElse(this.configRoot.connectTimeout); + Long connectTimeout = oneOf(clientConfigByClassName().connectTimeout(), + clientConfigByConfigKey().connectTimeout()).orElse(this.configRoot.connectTimeout()); if (connectTimeout != null) { builder.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS); } - Long readTimeout = oneOf(clientConfigByClassName().readTimeout, - clientConfigByConfigKey().readTimeout).orElse(this.configRoot.readTimeout); + Long readTimeout = oneOf(clientConfigByClassName().readTimeout(), + clientConfigByConfigKey().readTimeout()).orElse(this.configRoot.readTimeout()); if (readTimeout != null) { builder.readTimeout(readTimeout, TimeUnit.MILLISECONDS); } } protected void configureBaseUrl(RestClientBuilder builder) { - Optional baseUrlOptional = oneOf(clientConfigByClassName().uri, clientConfigByConfigKey().uri); + Optional baseUrlOptional = oneOf(clientConfigByClassName().uri(), clientConfigByConfigKey().uri()); if (baseUrlOptional.isEmpty()) { - baseUrlOptional = oneOf(clientConfigByClassName().url, clientConfigByConfigKey().url); + baseUrlOptional = oneOf(clientConfigByClassName().url(), clientConfigByConfigKey().url()); } if (((baseUriFromAnnotation == null) || baseUriFromAnnotation.isEmpty()) && baseUrlOptional.isEmpty()) { @@ -330,12 +332,12 @@ protected void configureBaseUrl(RestClientBuilder builder) { } } - private RestClientConfig clientConfigByConfigKey() { - return this.configRoot.getClientConfig(this.configKey); + private RestClientsConfig.RestClientConfig clientConfigByConfigKey() { + return this.configRoot.getClient(this.configKey); } - private RestClientConfig clientConfigByClassName() { - return this.configRoot.getClientConfig(this.proxyType); + private RestClientsConfig.RestClientConfig clientConfigByClassName() { + return this.configRoot.getClient(this.proxyType); } @SafeVarargs diff --git a/extensions/resteasy-classic/resteasy-client/runtime/src/test/java/io/quarkus/restclient/runtime/RestClientBaseTest.java b/extensions/resteasy-classic/resteasy-client/runtime/src/test/java/io/quarkus/restclient/runtime/RestClientBaseTest.java index 52f2b42a2aa3f2..589bb7f3ebd05e 100644 --- a/extensions/resteasy-classic/resteasy-client/runtime/src/test/java/io/quarkus/restclient/runtime/RestClientBaseTest.java +++ b/extensions/resteasy-classic/resteasy-client/runtime/src/test/java/io/quarkus/restclient/runtime/RestClientBaseTest.java @@ -1,5 +1,9 @@ package io.quarkus.restclient.runtime; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.eclipse.microprofile.rest.client.ext.QueryParamStyle.*; +import static org.mockito.Mockito.verify; + import java.io.IOException; import java.io.OutputStream; import java.net.MalformedURLException; @@ -11,8 +15,8 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.util.Arrays; -import java.util.Optional; -import java.util.concurrent.TimeUnit; +import java.util.HashMap; +import java.util.Map; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; @@ -22,15 +26,14 @@ import jakarta.ws.rs.client.ClientResponseFilter; import org.eclipse.microprofile.rest.client.RestClientBuilder; -import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsConfig; +import io.quarkus.runtime.configuration.ConfigUtils; public class RestClientBaseTest { @@ -83,8 +86,13 @@ public static void afterAll() { public void testClientSpecificConfigs() throws Exception { // given - RestClientsConfig configRoot = createSampleConfigRoot(); - configRoot.putClientConfig("test-client", createSampleClientConfig()); + RestClientsConfig configRoot = ConfigUtils.emptyConfigBuilder() + .setAddDefaultSources(false) + .withMapping(RestClientsConfig.class) + .withDefaultValues(createSampleConfigRoot()) + .withDefaultValues(createSampleClientConfig("test-client")) + .build() + .getConfigMapping(RestClientsConfig.class); // when @@ -98,26 +106,31 @@ public void testClientSpecificConfigs() throws Exception { // then - Mockito.verify(restClientBuilderMock).baseUrl(new URL("http://localhost")); - Mockito.verify(restClientBuilderMock).proxyAddress("host1", 123); - Mockito.verify(restClientBuilderMock).connectTimeout(100, TimeUnit.MILLISECONDS); - Mockito.verify(restClientBuilderMock).readTimeout(101, TimeUnit.MILLISECONDS); - Mockito.verify(restClientBuilderMock).hostnameVerifier(Mockito.any(MyHostnameVerifier1.class)); - Mockito.verify(restClientBuilderMock).property("resteasy.connectionTTL", Arrays.asList(102, TimeUnit.MILLISECONDS)); - Mockito.verify(restClientBuilderMock).property("resteasy.connectionPoolSize", 103); - Mockito.verify(restClientBuilderMock).followRedirects(true); - Mockito.verify(restClientBuilderMock).register(MyResponseFilter1.class); - Mockito.verify(restClientBuilderMock).queryParamStyle(QueryParamStyle.COMMA_SEPARATED); - - Mockito.verify(restClientBuilderMock).trustStore(Mockito.any()); - Mockito.verify(restClientBuilderMock).keyStore(Mockito.any(), Mockito.anyString()); + verify(restClientBuilderMock).baseUrl(new URL("http://localhost")); + verify(restClientBuilderMock).proxyAddress("host1", 123); + verify(restClientBuilderMock).connectTimeout(100, MILLISECONDS); + verify(restClientBuilderMock).readTimeout(101, MILLISECONDS); + verify(restClientBuilderMock).hostnameVerifier(Mockito.any(MyHostnameVerifier1.class)); + verify(restClientBuilderMock).property("resteasy.connectionTTL", Arrays.asList(102, MILLISECONDS)); + verify(restClientBuilderMock).property("resteasy.connectionPoolSize", 103); + verify(restClientBuilderMock).followRedirects(true); + verify(restClientBuilderMock).register(MyResponseFilter1.class); + verify(restClientBuilderMock).queryParamStyle(COMMA_SEPARATED); + + verify(restClientBuilderMock).trustStore(Mockito.any()); + verify(restClientBuilderMock).keyStore(Mockito.any(), Mockito.anyString()); } @Test public void testGlobalConfigs() throws MalformedURLException { // given - RestClientsConfig configRoot = createSampleConfigRoot(); + RestClientsConfig configRoot = ConfigUtils.emptyConfigBuilder() + .setAddDefaultSources(false) + .withMapping(RestClientsConfig.class) + .withDefaultValues(createSampleConfigRoot()) + .build() + .getConfigMapping(RestClientsConfig.class); // when @@ -131,69 +144,64 @@ public void testGlobalConfigs() throws MalformedURLException { // then - Mockito.verify(restClientBuilderMock).baseUrl(new URL("http://localhost:8080")); - Mockito.verify(restClientBuilderMock).proxyAddress("host2", 123); - Mockito.verify(restClientBuilderMock).connectTimeout(200, TimeUnit.MILLISECONDS); - Mockito.verify(restClientBuilderMock).readTimeout(201, TimeUnit.MILLISECONDS); - Mockito.verify(restClientBuilderMock).hostnameVerifier(Mockito.any(MyHostnameVerifier2.class)); - Mockito.verify(restClientBuilderMock).property("resteasy.connectionTTL", Arrays.asList(202, TimeUnit.MILLISECONDS)); - Mockito.verify(restClientBuilderMock).property("resteasy.connectionPoolSize", 203); - Mockito.verify(restClientBuilderMock).followRedirects(true); - Mockito.verify(restClientBuilderMock).register(MyResponseFilter2.class); - Mockito.verify(restClientBuilderMock).queryParamStyle(QueryParamStyle.MULTI_PAIRS); - - Mockito.verify(restClientBuilderMock).trustStore(Mockito.any()); - Mockito.verify(restClientBuilderMock).keyStore(Mockito.any(), Mockito.anyString()); + verify(restClientBuilderMock).baseUrl(new URL("http://localhost:8080")); + verify(restClientBuilderMock).proxyAddress("host2", 123); + verify(restClientBuilderMock).connectTimeout(200, MILLISECONDS); + verify(restClientBuilderMock).readTimeout(201, MILLISECONDS); + verify(restClientBuilderMock).hostnameVerifier(Mockito.any(MyHostnameVerifier2.class)); + verify(restClientBuilderMock).property("resteasy.connectionTTL", Arrays.asList(202, MILLISECONDS)); + verify(restClientBuilderMock).property("resteasy.connectionPoolSize", 203); + verify(restClientBuilderMock).followRedirects(true); + verify(restClientBuilderMock).register(MyResponseFilter2.class); + verify(restClientBuilderMock).queryParamStyle(MULTI_PAIRS); + + verify(restClientBuilderMock).trustStore(Mockito.any()); + verify(restClientBuilderMock).keyStore(Mockito.any(), Mockito.anyString()); } - private static RestClientsConfig createSampleConfigRoot() { - RestClientsConfig configRoot = new RestClientsConfig(); - - configRoot.proxyAddress = Optional.of("host2:123"); - configRoot.connectTimeout = 200L; - configRoot.readTimeout = 201L; - configRoot.hostnameVerifier = Optional.of("io.quarkus.restclient.runtime.RestClientBaseTest$MyHostnameVerifier2"); - configRoot.connectionTTL = Optional.of(202); - configRoot.connectionPoolSize = Optional.of(203); - configRoot.followRedirects = Optional.of(true); - configRoot.providers = Optional.of("io.quarkus.restclient.runtime.RestClientBaseTest$MyResponseFilter2"); - configRoot.queryParamStyle = Optional.of(QueryParamStyle.MULTI_PAIRS); - - configRoot.trustStore = Optional.of(truststorePath.toAbsolutePath().toString()); - configRoot.trustStorePassword = Optional.of("truststorePassword"); - configRoot.trustStoreType = Optional.of("JKS"); - configRoot.keyStore = Optional.of(keystorePath.toAbsolutePath().toString()); - configRoot.keyStorePassword = Optional.of("keystorePassword"); - configRoot.keyStoreType = Optional.of("JKS"); - - return configRoot; + private static Map createSampleConfigRoot() { + Map rootConfig = new HashMap<>(); + rootConfig.put("quarkus.rest-client.proxy-address", "host2:123"); + rootConfig.put("quarkus.rest-client.connect-timeout", "200"); + rootConfig.put("quarkus.rest-client.read-timeout", "201"); + rootConfig.put("quarkus.rest-client.hostname-verifier", + "io.quarkus.restclient.runtime.RestClientBaseTest$MyHostnameVerifier2"); + rootConfig.put("quarkus.rest-client.connection-ttl", "202"); + rootConfig.put("quarkus.rest-client.connection-pool-size", "203"); + rootConfig.put("quarkus.rest-client.follow-redirects", "true"); + rootConfig.put("quarkus.rest-client.providers", "io.quarkus.restclient.runtime.RestClientBaseTest$MyResponseFilter2"); + rootConfig.put("quarkus.rest-client.query-param-style", "multi-pairs"); + rootConfig.put("quarkus.rest-client.trust-store", truststorePath.toAbsolutePath().toString()); + rootConfig.put("quarkus.rest-client.trust-store-password", "truststorePassword"); + rootConfig.put("quarkus.rest-client.trust-store-type", "JKS"); + rootConfig.put("quarkus.rest-client.key-store", keystorePath.toAbsolutePath().toString()); + rootConfig.put("quarkus.rest-client.key-store-password", "keystorePassword"); + rootConfig.put("quarkus.rest-client.key-store-type", "JKS"); + return rootConfig; } - private static RestClientConfig createSampleClientConfig() { - RestClientConfig clientConfig = new RestClientConfig(); - + private static Map createSampleClientConfig(final String restClientName) { + Map clientConfig = new HashMap<>(); // properties only configurable via client config - clientConfig.url = Optional.of("http://localhost"); - clientConfig.uri = Optional.empty(); - + clientConfig.put("quarkus.rest-client." + restClientName + ".url", "http://localhost"); // properties that override configRoot counterparts - clientConfig.proxyAddress = Optional.of("host1:123"); - clientConfig.connectTimeout = Optional.of(100L); - clientConfig.readTimeout = Optional.of(101L); - clientConfig.hostnameVerifier = Optional.of("io.quarkus.restclient.runtime.RestClientBaseTest$MyHostnameVerifier1"); - clientConfig.connectionTTL = Optional.of(102); - clientConfig.connectionPoolSize = Optional.of(103); - clientConfig.followRedirects = Optional.of(true); - clientConfig.providers = Optional.of("io.quarkus.restclient.runtime.RestClientBaseTest$MyResponseFilter1"); - clientConfig.queryParamStyle = Optional.of(QueryParamStyle.COMMA_SEPARATED); - - clientConfig.trustStore = Optional.of(truststorePath.toAbsolutePath().toString()); - clientConfig.trustStorePassword = Optional.of("truststorePassword"); - clientConfig.trustStoreType = Optional.of("JKS"); - clientConfig.keyStore = Optional.of(keystorePath.toAbsolutePath().toString()); - clientConfig.keyStorePassword = Optional.of("keystorePassword"); - clientConfig.keyStoreType = Optional.of("JKS"); - + clientConfig.put("quarkus.rest-client." + restClientName + ".proxy-address", "host1:123"); + clientConfig.put("quarkus.rest-client." + restClientName + ".connect-timeout", "100"); + clientConfig.put("quarkus.rest-client." + restClientName + ".read-timeout", "101"); + clientConfig.put("quarkus.rest-client." + restClientName + ".hostname-verifier", + "io.quarkus.restclient.runtime.RestClientBaseTest$MyHostnameVerifier1"); + clientConfig.put("quarkus.rest-client." + restClientName + ".connection-ttl", "102"); + clientConfig.put("quarkus.rest-client." + restClientName + ".connection-pool-size", "103"); + clientConfig.put("quarkus.rest-client." + restClientName + ".follow-redirects", "true"); + clientConfig.put("quarkus.rest-client." + restClientName + ".providers", + "io.quarkus.restclient.runtime.RestClientBaseTest$MyResponseFilter1"); + clientConfig.put("quarkus.rest-client." + restClientName + ".query-param-style", "comma-separated"); + clientConfig.put("quarkus.rest-client." + restClientName + ".trust-store", truststorePath.toAbsolutePath().toString()); + clientConfig.put("quarkus.rest-client." + restClientName + ".trust-store-password", "truststorePassword"); + clientConfig.put("quarkus.rest-client." + restClientName + ".trust-store-type", "JKS"); + clientConfig.put("quarkus.rest-client." + restClientName + ".key-store", keystorePath.toAbsolutePath().toString()); + clientConfig.put("quarkus.rest-client." + restClientName + ".key-store-password", "keystorePassword"); + clientConfig.put("quarkus.rest-client." + restClientName + ".key-store-type", "JKS"); return clientConfig; } diff --git a/extensions/resteasy-classic/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/RestClientBuildItem.java b/extensions/resteasy-classic/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/RestClientBuildItem.java index cb7834d9beae91..33190120db3f5e 100644 --- a/extensions/resteasy-classic/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/RestClientBuildItem.java +++ b/extensions/resteasy-classic/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/RestClientBuildItem.java @@ -1,5 +1,10 @@ package io.quarkus.resteasy.common.deployment; +import java.util.Objects; +import java.util.Optional; + +import org.jboss.jandex.ClassInfo; + import io.quarkus.builder.item.MultiBuildItem; /** @@ -10,12 +15,29 @@ public final class RestClientBuildItem extends MultiBuildItem { private String interfaceName; + private final ClassInfo classInfo; + private final Optional configKey; + private final Optional defaultBaseUri; - public RestClientBuildItem(String interfaceName) { - this.interfaceName = interfaceName; + public RestClientBuildItem(ClassInfo classInfo, Optional configKey, Optional defaultBaseUri) { + this.classInfo = Objects.requireNonNull(classInfo); + this.configKey = Objects.requireNonNull(configKey); + this.defaultBaseUri = Objects.requireNonNull(defaultBaseUri); } public String getInterfaceName() { - return interfaceName; + return classInfo.name().toString(); + } + + public ClassInfo getClassInfo() { + return classInfo; + } + + public Optional getConfigKey() { + return configKey; + } + + public Optional getDefaultBaseUri() { + return defaultBaseUri; } } diff --git a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/QuarkusClientEndpointIndexer.java b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/QuarkusClientEndpointIndexer.java index ba76b9388c4e93..be5dce445bdb75 100644 --- a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/QuarkusClientEndpointIndexer.java +++ b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/QuarkusClientEndpointIndexer.java @@ -69,7 +69,7 @@ protected void handleAdditionalMethodProcessing(ResourceMethod method, ClassInfo protected void logMissingJsonWarning(MethodInfo info) { LOGGER.warnf("Quarkus detected the use of JSON in REST Client method '" + info.declaringClass().name() + "#" + info.name() - + "' but no JSON extension has been added. Consider adding 'quarkus-rest-client-reactive-jackson' (recommended) or 'quarkus-rest-client-reactive-jsonb'."); + + "' but no JSON extension has been added. Consider adding 'quarkus-rest-client-jackson' (recommended) or 'quarkus-rest-client-jsonb'."); } @Override diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java index c37475064b6625..219145afe0e835 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java @@ -87,6 +87,8 @@ import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; +import io.quarkus.deployment.builditem.StaticInitConfigBuilderBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem; @@ -105,6 +107,7 @@ import io.quarkus.rest.client.reactive.runtime.RestClientReactiveConfig; import io.quarkus.rest.client.reactive.runtime.RestClientRecorder; import io.quarkus.rest.client.reactive.spi.RestClientAnnotationsTransformerBuildItem; +import io.quarkus.restclient.config.RegisteredRestClient; import io.quarkus.restclient.config.RestClientsBuildTimeConfig; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.restclient.config.deployment.RestClientConfigUtils; @@ -428,7 +431,7 @@ void determineRegisteredRestClients(CombinedIndexBuildItem combinedIndexBuildIte } // now we go through the keys and if any of them correspond to classes that don't have a @RegisterRestClient annotation, we fake that annotation - Set configKeyNames = clientsConfig.configs.keySet(); + Set configKeyNames = clientsConfig.clients().keySet(); for (String configKeyName : configKeyNames) { ClassInfo classInfo = index.getClassByName(configKeyName); if (classInfo == null) { @@ -440,7 +443,7 @@ void determineRegisteredRestClients(CombinedIndexBuildItem combinedIndexBuildIte if (!Modifier.isAbstract(classInfo.flags())) { continue; } - Optional cdiScope = clientsConfig.configs.get(configKeyName).scope; + Optional cdiScope = clientsConfig.clients().get(configKeyName).scope(); if (cdiScope.isEmpty()) { continue; } @@ -448,6 +451,24 @@ void determineRegisteredRestClients(CombinedIndexBuildItem combinedIndexBuildIte } } + @BuildStep + void generateRestClientConfigBuilder( + List restClients, + BuildProducer generatedClass, + BuildProducer staticInitConfigBuilder, + BuildProducer runTimeConfigBuilder) { + + List registeredRestClients = restClients.stream() + .map(rc -> new RegisteredRestClient( + rc.getClassInfo().name().toString(), + rc.getClassInfo().simpleName(), + rc.getConfigKey().orElse(null))) + .toList(); + + RestClientConfigUtils.generateRestClientConfigBuilder(registeredRestClients, generatedClass, staticInitConfigBuilder, + runTimeConfigBuilder); + } + @BuildStep @Record(ExecutionTime.STATIC_INIT) void addRestClientBeans(Capabilities capabilities, diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/devservices/DevServicesRestClientHttpProxyProcessor.java b/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/devservices/DevServicesRestClientHttpProxyProcessor.java index 60ac73dac04ff6..a1a4d5c8f8fb87 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/devservices/DevServicesRestClientHttpProxyProcessor.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/devservices/DevServicesRestClientHttpProxyProcessor.java @@ -29,9 +29,8 @@ import io.quarkus.rest.client.reactive.deployment.RegisteredRestClientBuildItem; import io.quarkus.rest.client.reactive.spi.DevServicesRestClientProxyProvider; import io.quarkus.rest.client.reactive.spi.RestClientHttpProxyBuildItem; -import io.quarkus.restclient.config.RestClientBuildConfig; -import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsBuildTimeConfig; +import io.quarkus.restclient.config.RestClientsBuildTimeConfig.RestClientBuildConfig; @BuildSteps(onlyIfNot = IsNormal.class) public class DevServicesRestClientHttpProxyProcessor { @@ -56,15 +55,15 @@ public void determineRequiredProxies(RestClientsBuildTimeConfig restClientsBuild CombinedIndexBuildItem combinedIndexBuildItem, List registeredRestClientBuildItems, BuildProducer producer) { - if (restClientsBuildTimeConfig.configs.isEmpty()) { + if (restClientsBuildTimeConfig.clients().isEmpty()) { return; } IndexView index = combinedIndexBuildItem.getIndex(); - Map configs = restClientsBuildTimeConfig.configs; + Map configs = restClientsBuildTimeConfig.clients(); for (var configEntry : configs.entrySet()) { - if (!configEntry.getValue().enableLocalProxy) { + if (!configEntry.getValue().enableLocalProxy()) { log.trace("Ignoring config key: '" + configEntry.getKey() + "' because enableLocalProxy is false"); break; } @@ -81,8 +80,8 @@ public void determineRequiredProxies(RestClientsBuildTimeConfig restClientsBuild } if (matchingBI != null) { Optional baseUri = oneOf( - RestClientConfig.getConfigValue(configKey, "uri", String.class), - RestClientConfig.getConfigValue(configKey, "url", String.class), + Optional.of(configEntry.getValue().properties().get("url")), + Optional.of(configEntry.getValue().properties().get("url")), matchingBI.getDefaultBaseUri()); if (baseUri.isEmpty()) { @@ -90,7 +89,7 @@ public void determineRequiredProxies(RestClientsBuildTimeConfig restClientsBuild break; } producer.produce(new RestClientHttpProxyBuildItem(matchingBI.getClassInfo().name().toString(), baseUri.get(), - configEntry.getValue().localProxyProvider)); + configEntry.getValue().localProxyProvider())); } else { // now we check if the configKey was actually a class name ClassInfo classInfo = index.getClassByName(configKey); @@ -100,14 +99,14 @@ public void determineRequiredProxies(RestClientsBuildTimeConfig restClientsBuild break; } Optional baseUri = oneOf( - RestClientConfig.getConfigValue(configKey, "uri", String.class), - RestClientConfig.getConfigValue(configKey, "url", String.class)); + Optional.of(configEntry.getValue().properties().get("url")), + Optional.of(configEntry.getValue().properties().get("url"))); if (baseUri.isEmpty()) { log.debug("Unable to determine uri or url for config key '" + configKey + "'"); break; } producer.produce(new RestClientHttpProxyBuildItem(classInfo.name().toString(), baseUri.get(), - configEntry.getValue().localProxyProvider)); + configEntry.getValue().localProxyProvider())); } } } diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ConfigurationTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ConfigurationTest.java index eb6011ebd3ff07..53d7ce2e4a5d78 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ConfigurationTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ConfigurationTest.java @@ -2,11 +2,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Set; import jakarta.enterprise.inject.spi.Bean; import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; @@ -16,7 +18,7 @@ import io.quarkus.arc.Arc; import io.quarkus.rest.client.reactive.configuration.EchoResource; -import io.quarkus.restclient.config.RestClientConfig; +import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.test.QuarkusUnitTest; public class ConfigurationTest { @@ -29,9 +31,10 @@ public class ConfigurationTest { @RestClient HelloClientWithBaseUri client; - @RestClient EchoClientWithEmptyPath echoClientWithEmptyPath; + @Inject + RestClientsConfig restClientsConfig; @Test void shouldHaveSingletonScope() { @@ -48,21 +51,21 @@ void clientShouldRespond() { @Test void checkClientSpecificConfigs() { - RestClientConfig clientConfig = RestClientConfig.load(io.quarkus.rest.client.reactive.HelloClientWithBaseUri.class); + RestClientsConfig.RestClientConfig clientConfig = restClientsConfig.getClient(HelloClientWithBaseUri.class); verifyClientConfig(clientConfig, true); - clientConfig = RestClientConfig.load("client-prefix"); + clientConfig = restClientsConfig.getClient("client-prefix"); verifyClientConfig(clientConfig, true); - assertThat(clientConfig.proxyAddress.isPresent()).isTrue(); - assertThat(clientConfig.proxyAddress.get()).isEqualTo("localhost:8080"); - assertThat(clientConfig.headers).containsOnly(entry("user-agent", "MP REST Client"), entry("foo", "bar")); + assertThat(clientConfig.proxyAddress().isPresent()).isTrue(); + assertThat(clientConfig.proxyAddress().get()).isEqualTo("localhost:8080"); + assertThat(clientConfig.headers()).containsOnly(entry("user-agent", "MP REST Client"), entry("foo", "bar")); - clientConfig = RestClientConfig.load("quoted-client-prefix"); - assertThat(clientConfig.url.isPresent()).isTrue(); - assertThat(clientConfig.url.get()).endsWith("/hello"); - assertThat(clientConfig.headers).containsOnly(entry("foo", "bar")); + clientConfig = restClientsConfig.getClient("quoted-client-prefix"); + assertThat(clientConfig.url().isPresent()).isTrue(); + assertThat(clientConfig.url().get()).endsWith("/hello"); + assertThat(clientConfig.headers()).containsOnly(entry("foo", "bar")); - clientConfig = RestClientConfig.load("mp-client-prefix"); + clientConfig = restClientsConfig.getClient("mp-client-prefix"); verifyClientConfig(clientConfig, false); } @@ -71,30 +74,30 @@ void emptyPathAnnotationShouldWork() { assertThat(echoClientWithEmptyPath.echo("hello", "hello world")).isEqualTo("hello world"); } - private void verifyClientConfig(RestClientConfig clientConfig, boolean checkExtraProperties) { - assertThat(clientConfig.url).isPresent(); - assertThat(clientConfig.url.get()).endsWith("/hello"); - assertThat(clientConfig.providers).isPresent(); - assertThat(clientConfig.providers.get()) + private void verifyClientConfig(RestClientsConfig.RestClientConfig clientConfig, boolean checkExtraProperties) { + assertTrue(clientConfig.url().isPresent()); + assertThat(clientConfig.url().get()).endsWith("/hello"); + assertTrue(clientConfig.providers().isPresent()); + assertThat(clientConfig.providers().get()) .isEqualTo("io.quarkus.rest.client.reactive.HelloClientWithBaseUri$MyResponseFilter"); - assertThat(clientConfig.connectTimeout).isPresent(); - assertThat(clientConfig.connectTimeout.get()).isEqualTo(5000); - assertThat(clientConfig.readTimeout).isPresent(); - assertThat(clientConfig.readTimeout.get()).isEqualTo(6000); - assertThat(clientConfig.followRedirects).isPresent(); - assertThat(clientConfig.followRedirects.get()).isEqualTo(true); - assertThat(clientConfig.queryParamStyle).isPresent(); - assertThat(clientConfig.queryParamStyle.get()).isEqualTo(QueryParamStyle.COMMA_SEPARATED); + assertTrue(clientConfig.connectTimeout().isPresent()); + assertThat(clientConfig.connectTimeout().get()).isEqualTo(5000); + assertTrue(clientConfig.readTimeout().isPresent()); + assertThat(clientConfig.readTimeout().get()).isEqualTo(6000); + assertTrue(clientConfig.followRedirects().isPresent()); + assertThat(clientConfig.followRedirects().get()).isEqualTo(true); + assertTrue(clientConfig.queryParamStyle().isPresent()); + assertThat(clientConfig.queryParamStyle().get()).isEqualTo(QueryParamStyle.COMMA_SEPARATED); if (checkExtraProperties) { - assertThat(clientConfig.connectionTTL).isPresent(); - assertThat(clientConfig.connectionTTL.get()).isEqualTo(30000); - assertThat(clientConfig.connectionPoolSize).isPresent(); - assertThat(clientConfig.connectionPoolSize.get()).isEqualTo(10); - assertThat(clientConfig.keepAliveEnabled).isPresent(); - assertThat(clientConfig.keepAliveEnabled.get()).isFalse(); - assertThat(clientConfig.maxRedirects).isPresent(); - assertThat(clientConfig.maxRedirects.get()).isEqualTo(5); + assertTrue(clientConfig.connectionTTL().isPresent()); + assertThat(clientConfig.connectionTTL().get()).isEqualTo(30000); + assertTrue(clientConfig.connectionPoolSize().isPresent()); + assertThat(clientConfig.connectionPoolSize().get()).isEqualTo(10); + assertTrue(clientConfig.keepAliveEnabled().isPresent()); + assertThat(clientConfig.keepAliveEnabled().get()).isFalse(); + assertTrue(clientConfig.maxRedirects().isPresent()); + assertThat(clientConfig.maxRedirects().get()).isEqualTo(5); } } } diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/GlobalConfigurationTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/GlobalConfigurationTest.java index cf10f861712087..62f0426767e1d6 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/GlobalConfigurationTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/GlobalConfigurationTest.java @@ -50,34 +50,33 @@ void shouldRespond() { @Test void checkGlobalConfigValues() { // global properties: - assertThat(configRoot.multipartPostEncoderMode.get()).isEqualTo("HTML5"); - assertThat(configRoot.disableContextualErrorMessages).isTrue(); + assertThat(configRoot.multipartPostEncoderMode().get()).isEqualTo("HTML5"); + assertThat(configRoot.disableContextualErrorMessages()).isTrue(); // global defaults for client specific properties: - assertThat(configRoot.scope.get()).isEqualTo("Singleton"); - assertThat(configRoot.proxyAddress.get()).isEqualTo("host:123"); - assertThat(configRoot.proxyUser.get()).isEqualTo("proxyUser"); - assertThat(configRoot.proxyPassword.get()).isEqualTo("proxyPassword"); - assertThat(configRoot.nonProxyHosts.get()).isEqualTo("nonProxyHosts"); - assertThat(configRoot.connectTimeout).isEqualTo(2000); - assertThat(configRoot.readTimeout).isEqualTo(2001); - assertThat(configRoot.userAgent.get()).isEqualTo("agent"); - assertThat(configRoot.headers).isEqualTo(Collections.singletonMap("foo", "bar")); - assertThat(configRoot.connectionTTL.get()).isEqualTo(20000); // value in ms, will be converted to seconds - assertThat(configRoot.connectionPoolSize.get()).isEqualTo(2); - assertThat(configRoot.keepAliveEnabled.get()).isTrue(); - assertThat(configRoot.maxRedirects.get()).isEqualTo(2); - assertThat(configRoot.followRedirects.get()).isTrue(); - assertThat(configRoot.providers.get()) + assertThat(configRoot.scope().get()).isEqualTo("Singleton"); + assertThat(configRoot.proxyAddress().get()).isEqualTo("host:123"); + assertThat(configRoot.proxyUser().get()).isEqualTo("proxyUser"); + assertThat(configRoot.proxyPassword().get()).isEqualTo("proxyPassword"); + assertThat(configRoot.nonProxyHosts().get()).isEqualTo("nonProxyHosts"); + assertThat(configRoot.connectTimeout()).isEqualTo(2000); + assertThat(configRoot.readTimeout()).isEqualTo(2001); + assertThat(configRoot.userAgent().get()).isEqualTo("agent"); + assertThat(configRoot.headers()).isEqualTo(Collections.singletonMap("foo", "bar")); + assertThat(configRoot.connectionTTL().get()).isEqualTo(20000); // value in ms, will be converted to seconds + assertThat(configRoot.connectionPoolSize().get()).isEqualTo(2); + assertThat(configRoot.keepAliveEnabled().get()).isTrue(); + assertThat(configRoot.maxRedirects().get()).isEqualTo(2); + assertThat(configRoot.followRedirects().get()).isTrue(); + assertThat(configRoot.providers().get()) .isEqualTo("io.quarkus.rest.client.reactive.HelloClientWithBaseUri$MyResponseFilter"); - assertThat(configRoot.queryParamStyle.get()).isEqualTo(QueryParamStyle.MULTI_PAIRS); + assertThat(configRoot.queryParamStyle().get()).isEqualTo(QueryParamStyle.MULTI_PAIRS); - assertThat(configRoot.trustStore.get()).isEqualTo("/path"); - assertThat(configRoot.trustStorePassword.get()).isEqualTo("password"); - assertThat(configRoot.trustStoreType.get()).isEqualTo("JKS"); - assertThat(configRoot.keyStore.get()).isEqualTo("/path"); - assertThat(configRoot.keyStorePassword.get()).isEqualTo("password"); - assertThat(configRoot.keyStoreType.get()).isEqualTo("JKS"); + assertThat(configRoot.trustStore().get()).isEqualTo("/path"); + assertThat(configRoot.trustStorePassword().get()).isEqualTo("password"); + assertThat(configRoot.trustStoreType().get()).isEqualTo("JKS"); + assertThat(configRoot.keyStore().get()).isEqualTo("/path"); + assertThat(configRoot.keyStorePassword().get()).isEqualTo("password"); + assertThat(configRoot.keyStoreType().get()).isEqualTo("JKS"); } - } diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/LegacyConfigurationTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/LegacyConfigurationTest.java index 3075c625662ef9..9a058f424c1039 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/LegacyConfigurationTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/LegacyConfigurationTest.java @@ -1,6 +1,7 @@ package io.quarkus.rest.client.reactive; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; import jakarta.inject.Inject; @@ -8,7 +9,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.rest.client.reactive.configuration.EchoResource; -import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.test.QuarkusUnitTest; @@ -25,16 +25,15 @@ public class LegacyConfigurationTest { @Test void configurationShouldBeLoaded() { - assertThat(configRoot.multipartPostEncoderMode).isPresent(); - assertThat(configRoot.multipartPostEncoderMode.get()).isEqualTo("RFC3986"); + assertTrue(configRoot.multipartPostEncoderMode().isPresent()); + assertThat(configRoot.multipartPostEncoderMode().get()).isEqualTo("RFC3986"); - RestClientConfig clientConfig = RestClientConfig.load(io.quarkus.rest.client.reactive.HelloClient.class); - assertThat(clientConfig.maxRedirects).isPresent(); - assertThat(clientConfig.maxRedirects.get()).isEqualTo(4); + RestClientsConfig.RestClientConfig clientConfig = configRoot.getClient(HelloClient.class); + assertTrue(clientConfig.maxRedirects().isPresent()); + assertThat(clientConfig.maxRedirects().get()).isEqualTo(4); - clientConfig = RestClientConfig.load("client-prefix"); - assertThat(clientConfig.maxRedirects).isPresent(); - assertThat(clientConfig.maxRedirects.get()).isEqualTo(4); + clientConfig = configRoot.getClient("client-prefix"); + assertTrue(clientConfig.maxRedirects().isPresent()); + assertThat(clientConfig.maxRedirects().get()).isEqualTo(4); } - } diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index 5020e1bdcbf7cd..828034de5c9be9 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -45,10 +45,9 @@ import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; import io.quarkus.rest.client.reactive.runtime.ProxyAddressUtil.HostAndPort; -import io.quarkus.restclient.config.RestClientConfig; -import io.quarkus.restclient.config.RestClientLoggingConfig; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.tls.TlsConfiguration; +import io.smallrye.config.SmallRyeConfig; import io.vertx.core.net.KeyCertOptions; import io.vertx.core.net.SSLOptions; import io.vertx.core.net.TrustOptions; @@ -396,8 +395,11 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi "The Reactive REST Client needs to be built within the context of a Quarkus application with a valid ArC (CDI) context running."); } + SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class); + RestClientsConfig restClients = config.getConfigMapping(RestClientsConfig.class); + // support overriding the URI from the override-uri property - Optional maybeOverrideUri = RestClientConfig.getConfigValue(aClass, "override-uri", String.class); + Optional maybeOverrideUri = restClients.getClient(aClass).overrideUri(); if (maybeOverrideUri.isPresent()) { uri = URI.create(maybeOverrideUri.get()); } @@ -427,19 +429,17 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi clientBuilder.register(new MicroProfileRestClientResponseFilter(exceptionMappers)); clientBuilder.followRedirects(followRedirects); - RestClientsConfig restClientsConfig = arcContainer.instance(RestClientsConfig.class).get(); - - RestClientLoggingConfig logging = restClientsConfig.logging; + RestClientsConfig.RestClientLoggingConfig logging = restClients.logging(); LoggingScope effectiveLoggingScope = loggingScope; // if a scope was specified programmatically, it takes precedence if (effectiveLoggingScope == null) { - effectiveLoggingScope = logging != null ? logging.scope.map(LoggingScope::forName).orElse(LoggingScope.NONE) + effectiveLoggingScope = logging != null ? logging.scope().map(LoggingScope::forName).orElse(LoggingScope.NONE) : LoggingScope.NONE; } Integer effectiveLoggingBodyLimit = loggingBodyLimit; // if a limit was specified programmatically, it takes precedence if (effectiveLoggingBodyLimit == null) { - effectiveLoggingBodyLimit = logging != null ? logging.bodyLimit : 100; + effectiveLoggingBodyLimit = logging != null ? logging.bodyLimit() : 100; } clientBuilder.loggingScope(effectiveLoggingScope); clientBuilder.loggingBodySize(effectiveLoggingBodyLimit); @@ -461,36 +461,36 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi } clientBuilder.trustAll(effectiveTrustAll); - restClientsConfig.verifyHost.ifPresent(clientBuilder::verifyHost); + restClients.verifyHost().ifPresent(clientBuilder::verifyHost); String effectiveUserAgent = userAgent; if (effectiveUserAgent != null) { clientBuilder.setUserAgent(effectiveUserAgent); - } else if (restClientsConfig.userAgent.isPresent()) { // if config set and client obtained programmatically - clientBuilder.setUserAgent(restClientsConfig.userAgent.get()); + } else if (restClients.userAgent().isPresent()) { // if config set and client obtained programmatically + clientBuilder.setUserAgent(restClients.userAgent().get()); } Integer maxChunkSize = (Integer) getConfiguration().getProperty(QuarkusRestClientProperties.MAX_CHUNK_SIZE); if (maxChunkSize != null) { clientBuilder.maxChunkSize(maxChunkSize); - } else if (restClientsConfig.maxChunkSize.isPresent()) { - clientBuilder.maxChunkSize((int) restClientsConfig.maxChunkSize.get().asLongValue()); - } else if (restClientsConfig.multipart.maxChunkSize.isPresent()) { - clientBuilder.maxChunkSize(restClientsConfig.multipart.maxChunkSize.get()); + } else if (restClients.maxChunkSize().isPresent()) { + clientBuilder.maxChunkSize((int) restClients.maxChunkSize().get().asLongValue()); + } else if (restClients.multipart().maxChunkSize().isPresent()) { + clientBuilder.maxChunkSize(restClients.multipart().maxChunkSize().get()); } else { clientBuilder.maxChunkSize(DEFAULT_MAX_CHUNK_SIZE); } if (getConfiguration().hasProperty(QuarkusRestClientProperties.HTTP2)) { clientBuilder.http2((Boolean) getConfiguration().getProperty(QuarkusRestClientProperties.HTTP2)); - } else if (restClientsConfig.http2) { + } else if (restClients.http2()) { clientBuilder.http2(true); } if (getConfiguration().hasProperty(QuarkusRestClientProperties.ALPN)) { clientBuilder.alpn((Boolean) getConfiguration().getProperty(QuarkusRestClientProperties.ALPN)); - } else if (restClientsConfig.alpn.isPresent()) { - clientBuilder.alpn(restClientsConfig.alpn.get()); + } else if (restClients.alpn().isPresent()) { + clientBuilder.alpn(restClients.alpn().get()); } Boolean enableCompression = ConfigProvider.getConfig() @@ -501,10 +501,10 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi if (proxyHost != null) { configureProxy(proxyHost, proxyPort, proxyUser, proxyPassword, nonProxyHosts); - } else if (restClientsConfig.proxyAddress.isPresent()) { - HostAndPort globalProxy = ProxyAddressUtil.parseAddress(restClientsConfig.proxyAddress.get()); - configureProxy(globalProxy.host, globalProxy.port, restClientsConfig.proxyUser.orElse(null), - restClientsConfig.proxyPassword.orElse(null), restClientsConfig.nonProxyHosts.orElse(null)); + } else if (restClients.proxyAddress().isPresent()) { + HostAndPort globalProxy = ProxyAddressUtil.parseAddress(restClients.proxyAddress().get()); + configureProxy(globalProxy.host, globalProxy.port, restClients.proxyUser().orElse(null), + restClients.proxyPassword().orElse(null), restClients.nonProxyHosts().orElse(null)); } if (!clientBuilder.getConfiguration().hasProperty(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE)) { @@ -512,9 +512,9 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi if (this.multipartPostEncoderMode != null) { multipartPostEncoderMode = PausableHttpPostRequestEncoder.EncoderMode .valueOf(this.multipartPostEncoderMode.toUpperCase(Locale.ROOT)); - } else if (restClientsConfig.multipartPostEncoderMode.isPresent()) { + } else if (restClients.multipartPostEncoderMode().isPresent()) { multipartPostEncoderMode = PausableHttpPostRequestEncoder.EncoderMode - .valueOf(restClientsConfig.multipartPostEncoderMode.get().toUpperCase(Locale.ROOT)); + .valueOf(restClients.multipartPostEncoderMode().get().toUpperCase(Locale.ROOT)); } if (multipartPostEncoderMode != null) { clientBuilder.property(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE, multipartPostEncoderMode); diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java index 186ca14629a67f..e1d5eb53db6701 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java @@ -22,17 +22,18 @@ import javax.net.ssl.HostnameVerifier; +import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; import org.jboss.resteasy.reactive.client.impl.multipart.PausableHttpPostRequestEncoder; import io.quarkus.arc.Arc; import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; -import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.runtime.configuration.MemorySize; import io.quarkus.tls.TlsConfiguration; import io.quarkus.tls.TlsConfigurationRegistry; +import io.smallrye.config.SmallRyeConfig; public class RestClientCDIDelegateBuilder { @@ -50,7 +51,8 @@ public static T createDelegate(Class jaxrsInterface, String baseUriFromAn } private RestClientCDIDelegateBuilder(Class jaxrsInterface, String baseUriFromAnnotation, String configKey) { - this(jaxrsInterface, baseUriFromAnnotation, configKey, RestClientsConfig.getInstance()); + this(jaxrsInterface, baseUriFromAnnotation, configKey, + ConfigProvider.getConfig().unwrap(SmallRyeConfig.class).getConfigMapping(RestClientsConfig.class)); } RestClientCDIDelegateBuilder(Class jaxrsInterface, String baseUriFromAnnotation, String configKey, @@ -80,75 +82,75 @@ void configureBuilder(QuarkusRestClientBuilder builder) { } private void configureCustomProperties(QuarkusRestClientBuilder builder) { - Optional encoder = oneOf(clientConfigByClassName().multipartPostEncoderMode, - clientConfigByConfigKey().multipartPostEncoderMode, configRoot.multipartPostEncoderMode); + Optional encoder = oneOf(clientConfigByClassName().multipartPostEncoderMode(), + clientConfigByConfigKey().multipartPostEncoderMode(), configRoot.multipartPostEncoderMode()); if (encoder != null && encoder.isPresent()) { PausableHttpPostRequestEncoder.EncoderMode mode = PausableHttpPostRequestEncoder.EncoderMode .valueOf(encoder.get().toUpperCase(Locale.ROOT)); builder.property(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE, mode); } - Optional poolSize = oneOf(clientConfigByClassName().connectionPoolSize, - clientConfigByConfigKey().connectionPoolSize, configRoot.connectionPoolSize); + Optional poolSize = oneOf(clientConfigByClassName().connectionPoolSize(), + clientConfigByConfigKey().connectionPoolSize(), configRoot.connectionPoolSize()); if (poolSize.isPresent()) { builder.property(QuarkusRestClientProperties.CONNECTION_POOL_SIZE, poolSize.get()); } - Optional connectionTTL = oneOf(clientConfigByClassName().connectionTTL, - clientConfigByConfigKey().connectionTTL, configRoot.connectionTTL); + Optional connectionTTL = oneOf(clientConfigByClassName().connectionTTL(), + clientConfigByConfigKey().connectionTTL(), configRoot.connectionTTL()); if (connectionTTL.isPresent()) { // configuration bean contains value in milliseconds int connectionTTLSeconds = connectionTTL.get() / 1000; builder.property(QuarkusRestClientProperties.CONNECTION_TTL, connectionTTLSeconds); } - Optional keepAliveEnabled = oneOf(clientConfigByClassName().keepAliveEnabled, - clientConfigByConfigKey().keepAliveEnabled, configRoot.keepAliveEnabled); + Optional keepAliveEnabled = oneOf(clientConfigByClassName().keepAliveEnabled(), + clientConfigByConfigKey().keepAliveEnabled(), configRoot.keepAliveEnabled()); if (keepAliveEnabled.isPresent()) { builder.property(QuarkusRestClientProperties.KEEP_ALIVE_ENABLED, keepAliveEnabled.get()); } - Map headers = clientConfigByClassName().headers; + Map headers = clientConfigByClassName().headers(); if (headers == null || headers.isEmpty()) { - headers = clientConfigByConfigKey().headers; + headers = clientConfigByConfigKey().headers(); } if (headers == null || headers.isEmpty()) { - headers = configRoot.headers; + headers = configRoot.headers(); } if (headers != null && !headers.isEmpty()) { builder.property(QuarkusRestClientProperties.STATIC_HEADERS, headers); } builder.property(QuarkusRestClientProperties.DISABLE_CONTEXTUAL_ERROR_MESSAGES, - configRoot.disableContextualErrorMessages); + configRoot.disableContextualErrorMessages()); - Optional userAgent = oneOf(clientConfigByClassName().userAgent, - clientConfigByConfigKey().userAgent, configRoot.userAgent); + Optional userAgent = oneOf(clientConfigByClassName().userAgent(), + clientConfigByConfigKey().userAgent(), configRoot.userAgent()); if (userAgent.isPresent()) { builder.userAgent(userAgent.get()); } Optional maxChunkSize = oneOf( - clientConfigByClassName().maxChunkSize.map(intChunkSize()), - clientConfigByClassName().multipart.maxChunkSize, - clientConfigByConfigKey().maxChunkSize.map(intChunkSize()), - clientConfigByConfigKey().multipart.maxChunkSize, - configRoot.maxChunkSize.map(intChunkSize()), - configRoot.multipart.maxChunkSize); + clientConfigByClassName().maxChunkSize().map(intChunkSize()), + clientConfigByClassName().multipart().maxChunkSize(), + clientConfigByConfigKey().maxChunkSize().map(intChunkSize()), + clientConfigByConfigKey().multipart().maxChunkSize(), + configRoot.maxChunkSize().map(intChunkSize()), + configRoot.multipart().maxChunkSize()); builder.property(QuarkusRestClientProperties.MAX_CHUNK_SIZE, maxChunkSize.orElse(DEFAULT_MAX_CHUNK_SIZE)); - Boolean http2 = oneOf(clientConfigByClassName().http2, - clientConfigByConfigKey().http2).orElse(configRoot.http2); + Boolean http2 = oneOf(clientConfigByClassName().http2(), + clientConfigByConfigKey().http2()).orElse(configRoot.http2()); builder.property(QuarkusRestClientProperties.HTTP2, http2); - Optional alpn = oneOf(clientConfigByClassName().alpn, - clientConfigByConfigKey().alpn, configRoot.alpn); + Optional alpn = oneOf(clientConfigByClassName().alpn(), + clientConfigByConfigKey().alpn(), configRoot.alpn()); if (alpn.isPresent()) { builder.property(QuarkusRestClientProperties.ALPN, alpn.get()); } - Boolean captureStacktrace = oneOf(clientConfigByClassName().captureStacktrace, - clientConfigByConfigKey().captureStacktrace).orElse(configRoot.captureStacktrace); + Boolean captureStacktrace = oneOf(clientConfigByClassName().captureStacktrace(), + clientConfigByConfigKey().captureStacktrace()).orElse(configRoot.captureStacktrace()); builder.property(QuarkusRestClientProperties.CAPTURE_STACKTRACE, captureStacktrace); } @@ -157,8 +159,8 @@ private static Function intChunkSize() { } private void configureProxy(QuarkusRestClientBuilder builder) { - Optional maybeProxy = oneOf(clientConfigByClassName().proxyAddress, clientConfigByConfigKey().proxyAddress, - configRoot.proxyAddress); + Optional maybeProxy = oneOf(clientConfigByClassName().proxyAddress(), clientConfigByConfigKey().proxyAddress(), + configRoot.proxyAddress()); if (maybeProxy.isEmpty()) { return; } @@ -170,18 +172,20 @@ private void configureProxy(QuarkusRestClientBuilder builder) { ProxyAddressUtil.HostAndPort hostAndPort = ProxyAddressUtil.parseAddress(proxyAddress); builder.proxyAddress(hostAndPort.host, hostAndPort.port); - oneOf(clientConfigByClassName().proxyUser, clientConfigByConfigKey().proxyUser, configRoot.proxyUser) + oneOf(clientConfigByClassName().proxyUser(), clientConfigByConfigKey().proxyUser(), configRoot.proxyUser()) .ifPresent(builder::proxyUser); - oneOf(clientConfigByClassName().proxyPassword, clientConfigByConfigKey().proxyPassword, configRoot.proxyPassword) + oneOf(clientConfigByClassName().proxyPassword(), clientConfigByConfigKey().proxyPassword(), + configRoot.proxyPassword()) .ifPresent(builder::proxyPassword); - oneOf(clientConfigByClassName().nonProxyHosts, clientConfigByConfigKey().nonProxyHosts, configRoot.nonProxyHosts) + oneOf(clientConfigByClassName().nonProxyHosts(), clientConfigByConfigKey().nonProxyHosts(), + configRoot.nonProxyHosts()) .ifPresent(builder::nonProxyHosts); } } private void configureQueryParamStyle(QuarkusRestClientBuilder builder) { - Optional maybeQueryParamStyle = oneOf(clientConfigByClassName().queryParamStyle, - clientConfigByConfigKey().queryParamStyle, configRoot.queryParamStyle); + Optional maybeQueryParamStyle = oneOf(clientConfigByClassName().queryParamStyle(), + clientConfigByConfigKey().queryParamStyle(), configRoot.queryParamStyle()); if (maybeQueryParamStyle.isPresent()) { QueryParamStyle queryParamStyle = maybeQueryParamStyle.get(); builder.queryParamStyle(queryParamStyle); @@ -189,29 +193,29 @@ private void configureQueryParamStyle(QuarkusRestClientBuilder builder) { } private void configureRedirects(QuarkusRestClientBuilder builder) { - Optional maxRedirects = oneOf(clientConfigByClassName().maxRedirects, - clientConfigByConfigKey().maxRedirects, configRoot.maxRedirects); + Optional maxRedirects = oneOf(clientConfigByClassName().maxRedirects(), + clientConfigByConfigKey().maxRedirects(), configRoot.maxRedirects()); if (maxRedirects.isPresent()) { builder.property(QuarkusRestClientProperties.MAX_REDIRECTS, maxRedirects.get()); } - Optional maybeFollowRedirects = oneOf(clientConfigByClassName().followRedirects, - clientConfigByConfigKey().followRedirects, configRoot.followRedirects); + Optional maybeFollowRedirects = oneOf(clientConfigByClassName().followRedirects(), + clientConfigByConfigKey().followRedirects(), configRoot.followRedirects()); if (maybeFollowRedirects.isPresent()) { builder.followRedirects(maybeFollowRedirects.get()); } } private void configureShared(QuarkusRestClientBuilder builder) { - Optional shared = oneOf(clientConfigByClassName().shared, - clientConfigByConfigKey().shared); + Optional shared = oneOf(clientConfigByClassName().shared(), + clientConfigByConfigKey().shared()); if (shared.isPresent()) { builder.property(QuarkusRestClientProperties.SHARED, shared.get()); if (shared.get()) { // Name is only used if shared = true - Optional name = oneOf(clientConfigByClassName().name, - clientConfigByConfigKey().name); + Optional name = oneOf(clientConfigByClassName().name(), + clientConfigByConfigKey().name()); if (name.isPresent()) { builder.property(QuarkusRestClientProperties.NAME, name.get()); } @@ -232,9 +236,9 @@ private Optional resolveTlsConfigurationForRegistry() { if (Arc.container() != null) { var registry = Arc.container().select(TlsConfigurationRegistry.class).orNull(); if (registry != null) { - Optional maybeTlsConfigurationName = oneOf(clientConfigByClassName().tlsConfigurationName, - clientConfigByConfigKey().tlsConfigurationName, - configRoot.tlsConfigurationName); + Optional maybeTlsConfigurationName = oneOf(clientConfigByClassName().tlsConfigurationName(), + clientConfigByConfigKey().tlsConfigurationName(), + configRoot.tlsConfigurationName()); return TlsConfiguration.from(registry, maybeTlsConfigurationName); } } @@ -242,25 +246,25 @@ private Optional resolveTlsConfigurationForRegistry() { } private void configureTLSFromProperties(QuarkusRestClientBuilder builder) { - Optional maybeTrustStore = oneOf(clientConfigByClassName().trustStore, clientConfigByConfigKey().trustStore, - configRoot.trustStore); + Optional maybeTrustStore = oneOf(clientConfigByClassName().trustStore(), clientConfigByConfigKey().trustStore(), + configRoot.trustStore()); if (maybeTrustStore.isPresent() && !maybeTrustStore.get().isBlank() && !NONE.equals(maybeTrustStore.get())) { registerTrustStore(maybeTrustStore.get(), builder); } - Optional maybeKeyStore = oneOf(clientConfigByClassName().keyStore, clientConfigByConfigKey().keyStore, - configRoot.keyStore); + Optional maybeKeyStore = oneOf(clientConfigByClassName().keyStore(), clientConfigByConfigKey().keyStore(), + configRoot.keyStore()); if (maybeKeyStore.isPresent() && !maybeKeyStore.get().isBlank() && !NONE.equals(maybeKeyStore.get())) { registerKeyStore(maybeKeyStore.get(), builder); } - Optional maybeHostnameVerifier = oneOf(clientConfigByClassName().hostnameVerifier, - clientConfigByConfigKey().hostnameVerifier, configRoot.hostnameVerifier); + Optional maybeHostnameVerifier = oneOf(clientConfigByClassName().hostnameVerifier(), + clientConfigByConfigKey().hostnameVerifier(), configRoot.hostnameVerifier()); if (maybeHostnameVerifier.isPresent()) { registerHostnameVerifier(maybeHostnameVerifier.get(), builder); } - oneOf(clientConfigByClassName().verifyHost, clientConfigByConfigKey().verifyHost, configRoot.verifyHost) + oneOf(clientConfigByClassName().verifyHost(), clientConfigByConfigKey().verifyHost(), configRoot.verifyHost()) .ifPresent(builder::verifyHost); } @@ -285,10 +289,10 @@ private void registerHostnameVerifier(String verifier, QuarkusRestClientBuilder } private void registerKeyStore(String keyStorePath, QuarkusRestClientBuilder builder) { - Optional keyStorePassword = oneOf(clientConfigByClassName().keyStorePassword, - clientConfigByConfigKey().keyStorePassword, configRoot.keyStorePassword); - Optional keyStoreType = oneOf(clientConfigByClassName().keyStoreType, - clientConfigByConfigKey().keyStoreType, configRoot.keyStoreType); + Optional keyStorePassword = oneOf(clientConfigByClassName().keyStorePassword(), + clientConfigByConfigKey().keyStorePassword(), configRoot.keyStorePassword()); + Optional keyStoreType = oneOf(clientConfigByClassName().keyStoreType(), + clientConfigByConfigKey().keyStoreType(), configRoot.keyStoreType()); try { KeyStore keyStore = KeyStore.getInstance(keyStoreType.orElse("JKS")); @@ -311,10 +315,10 @@ private void registerKeyStore(String keyStorePath, QuarkusRestClientBuilder buil } private void registerTrustStore(String trustStorePath, QuarkusRestClientBuilder builder) { - Optional maybeTrustStorePassword = oneOf(clientConfigByClassName().trustStorePassword, - clientConfigByConfigKey().trustStorePassword, configRoot.trustStorePassword); - Optional maybeTrustStoreType = oneOf(clientConfigByClassName().trustStoreType, - clientConfigByConfigKey().trustStoreType, configRoot.trustStoreType); + Optional maybeTrustStorePassword = oneOf(clientConfigByClassName().trustStorePassword(), + clientConfigByConfigKey().trustStorePassword(), configRoot.trustStorePassword()); + Optional maybeTrustStoreType = oneOf(clientConfigByClassName().trustStoreType(), + clientConfigByConfigKey().trustStoreType(), configRoot.trustStoreType()); try { KeyStore trustStore = KeyStore.getInstance(maybeTrustStoreType.orElse("JKS")); @@ -362,8 +366,8 @@ private InputStream locateStream(String path) throws FileNotFoundException { } private void configureProviders(QuarkusRestClientBuilder builder) { - Optional maybeProviders = oneOf(clientConfigByClassName().providers, clientConfigByConfigKey().providers, - configRoot.providers); + Optional maybeProviders = oneOf(clientConfigByClassName().providers(), clientConfigByConfigKey().providers(), + configRoot.providers()); if (maybeProviders.isPresent()) { registerProviders(builder, maybeProviders.get()); } @@ -384,26 +388,26 @@ private Class providerClassForName(String name) { } private void configureTimeouts(QuarkusRestClientBuilder builder) { - Long connectTimeout = oneOf(clientConfigByClassName().connectTimeout, - clientConfigByConfigKey().connectTimeout).orElse(this.configRoot.connectTimeout); + Long connectTimeout = oneOf(clientConfigByClassName().connectTimeout(), + clientConfigByConfigKey().connectTimeout()).orElse(this.configRoot.connectTimeout()); if (connectTimeout != null) { builder.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS); } - Long readTimeout = oneOf(clientConfigByClassName().readTimeout, - clientConfigByConfigKey().readTimeout).orElse(this.configRoot.readTimeout); + Long readTimeout = oneOf(clientConfigByClassName().readTimeout(), + clientConfigByConfigKey().readTimeout()).orElse(this.configRoot.readTimeout()); if (readTimeout != null) { builder.readTimeout(readTimeout, TimeUnit.MILLISECONDS); } } private void configureBaseUrl(QuarkusRestClientBuilder builder) { - Optional propertyOptional = oneOf(clientConfigByClassName().uri, - clientConfigByConfigKey().uri); + Optional propertyOptional = oneOf(clientConfigByClassName().uri(), + clientConfigByConfigKey().uri()); if (propertyOptional.isEmpty()) { - propertyOptional = oneOf(clientConfigByClassName().url, - clientConfigByConfigKey().url); + propertyOptional = oneOf(clientConfigByClassName().url(), + clientConfigByConfigKey().url()); } if (((baseUriFromAnnotation == null) || baseUriFromAnnotation.isEmpty()) && propertyOptional.isEmpty()) { @@ -425,12 +429,12 @@ private void configureBaseUrl(QuarkusRestClientBuilder builder) { } } - private RestClientConfig clientConfigByConfigKey() { - return this.configRoot.getClientConfig(configKey); + private RestClientsConfig.RestClientConfig clientConfigByConfigKey() { + return this.configRoot.getClient(configKey); } - private RestClientConfig clientConfigByClassName() { - return this.configRoot.getClientConfig(jaxrsInterface); + private RestClientsConfig.RestClientConfig clientConfigByClassName() { + return this.configRoot.getClient(jaxrsInterface); } @SafeVarargs diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/spi/JsonMissingMessageBodyReaderErrorMessageContextualizer.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/spi/JsonMissingMessageBodyReaderErrorMessageContextualizer.java index f5ffb518b63bea..50211277ac8e21 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/spi/JsonMissingMessageBodyReaderErrorMessageContextualizer.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/spi/JsonMissingMessageBodyReaderErrorMessageContextualizer.java @@ -9,7 +9,7 @@ public class JsonMissingMessageBodyReaderErrorMessageContextualizer implements @Override public String provideContextMessage(Input input) { if ((input.mediaType() != null) && input.mediaType().isCompatible(MediaType.APPLICATION_JSON_TYPE)) { - return "Consider adding one the 'quarkus-rest-client-reactive-jackson' or 'quarkus-rest-client-reactive-jsonb' extensions"; + return "Consider adding one the 'quarkus-rest-client-jackson' or 'quarkus-rest-client-jsonb' extensions"; } return null; } diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/spi/XmlMissingMessageBodyReaderErrorMessageContextualizer.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/spi/XmlMissingMessageBodyReaderErrorMessageContextualizer.java index 96927126ca4427..bfb4b068f2f323 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/spi/XmlMissingMessageBodyReaderErrorMessageContextualizer.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/spi/XmlMissingMessageBodyReaderErrorMessageContextualizer.java @@ -9,7 +9,7 @@ public class XmlMissingMessageBodyReaderErrorMessageContextualizer implements @Override public String provideContextMessage(Input input) { if ((input.mediaType() != null) && input.mediaType().isCompatible(MediaType.APPLICATION_XML_TYPE)) { - return "Consider adding the 'quarkus-rest-client-reactive-jaxb' extension"; + return "Consider adding the 'quarkus-rest-client-jaxb' extension"; } return null; } diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java b/extensions/resteasy-reactive/rest-client/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java index 9ba83692374bff..95eaf821219d5d 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java @@ -1,8 +1,21 @@ package io.quarkus.rest.client.reactive.runtime; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.eclipse.microprofile.rest.client.ext.QueryParamStyle.COMMA_SEPARATED; +import static org.eclipse.microprofile.rest.client.ext.QueryParamStyle.MULTI_PAIRS; +import static org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties.CONNECTION_POOL_SIZE; +import static org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties.CONNECTION_TTL; +import static org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties.DISABLE_CONTEXTUAL_ERROR_MESSAGES; +import static org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties.KEEP_ALIVE_ENABLED; +import static org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties.MAX_CHUNK_SIZE; +import static org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties.MAX_REDIRECTS; +import static org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties.MULTIPART_ENCODER_MODE; +import static org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties.STATIC_HEADERS; +import static org.jboss.resteasy.reactive.client.impl.multipart.PausableHttpPostRequestEncoder.EncoderMode.HTML5; +import static org.mockito.Mockito.verify; + import java.io.IOException; import java.io.OutputStream; -import java.math.BigInteger; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; @@ -10,27 +23,22 @@ import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; -import java.util.Collections; -import java.util.Optional; -import java.util.concurrent.TimeUnit; +import java.util.HashMap; +import java.util.Map; import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.client.ClientResponseContext; import jakarta.ws.rs.client.ClientResponseFilter; -import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; -import org.jboss.resteasy.reactive.client.impl.multipart.PausableHttpPostRequestEncoder; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import io.quarkus.restclient.config.RestClientConfig; -import io.quarkus.restclient.config.RestClientMultipartConfig; import io.quarkus.restclient.config.RestClientsConfig; -import io.quarkus.runtime.configuration.MemorySize; +import io.quarkus.runtime.configuration.ConfigUtils; @SuppressWarnings({ "SameParameterValue" }) public class RestClientCDIDelegateBuilderTest { @@ -84,8 +92,13 @@ public static void afterAll() { public void testClientSpecificConfigs() { // given - RestClientsConfig configRoot = createSampleConfigRoot(); - configRoot.putClientConfig("test-client", createSampleClientConfig()); + RestClientsConfig configRoot = ConfigUtils.emptyConfigBuilder() + .setAddDefaultSources(false) + .withMapping(RestClientsConfig.class) + .withDefaultValues(createSampleConfigRoot()) + .withDefaultValues(createSampleClientConfig("test-client")) + .build() + .getConfigMapping(RestClientsConfig.class); // when @@ -97,39 +110,42 @@ public void testClientSpecificConfigs() { // then - Mockito.verify(restClientBuilderMock).baseUri(URI.create("http://localhost")); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.SHARED, true); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.NAME, "my-client"); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE, - PausableHttpPostRequestEncoder.EncoderMode.HTML5); - - Mockito.verify(restClientBuilderMock).proxyAddress("host1", 123); - Mockito.verify(restClientBuilderMock).proxyUser("proxyUser1"); - Mockito.verify(restClientBuilderMock).proxyPassword("proxyPassword1"); - Mockito.verify(restClientBuilderMock).nonProxyHosts("nonProxyHosts1"); - Mockito.verify(restClientBuilderMock).connectTimeout(100, TimeUnit.MILLISECONDS); - Mockito.verify(restClientBuilderMock).readTimeout(101, TimeUnit.MILLISECONDS); - Mockito.verify(restClientBuilderMock).userAgent("agent1"); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.STATIC_HEADERS, - Collections.singletonMap("header1", "value")); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.CONNECTION_TTL, 10); // value converted to seconds - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.CONNECTION_POOL_SIZE, 103); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.KEEP_ALIVE_ENABLED, false); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.MAX_REDIRECTS, 104); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.MAX_CHUNK_SIZE, 1024); - Mockito.verify(restClientBuilderMock).followRedirects(true); - Mockito.verify(restClientBuilderMock).register(MyResponseFilter1.class); - Mockito.verify(restClientBuilderMock).queryParamStyle(QueryParamStyle.COMMA_SEPARATED); - - Mockito.verify(restClientBuilderMock).trustStore(Mockito.any(), Mockito.anyString()); - Mockito.verify(restClientBuilderMock).keyStore(Mockito.any(), Mockito.anyString()); + verify(restClientBuilderMock).baseUri(URI.create("http://localhost")); + verify(restClientBuilderMock).property(QuarkusRestClientProperties.SHARED, true); + verify(restClientBuilderMock).property(QuarkusRestClientProperties.NAME, "my-client"); + verify(restClientBuilderMock).property(MULTIPART_ENCODER_MODE, HTML5); + + verify(restClientBuilderMock).proxyAddress("host1", 123); + verify(restClientBuilderMock).proxyUser("proxyUser1"); + verify(restClientBuilderMock).proxyPassword("proxyPassword1"); + verify(restClientBuilderMock).nonProxyHosts("nonProxyHosts1"); + verify(restClientBuilderMock).connectTimeout(100, MILLISECONDS); + verify(restClientBuilderMock).readTimeout(101, MILLISECONDS); + verify(restClientBuilderMock).userAgent("agent1"); + verify(restClientBuilderMock).property(STATIC_HEADERS, Map.of("header1", "value")); + verify(restClientBuilderMock).property(CONNECTION_TTL, 10); // value converted to seconds + verify(restClientBuilderMock).property(CONNECTION_POOL_SIZE, 103); + verify(restClientBuilderMock).property(KEEP_ALIVE_ENABLED, false); + verify(restClientBuilderMock).property(MAX_REDIRECTS, 104); + verify(restClientBuilderMock).property(MAX_CHUNK_SIZE, 1024); + verify(restClientBuilderMock).followRedirects(true); + verify(restClientBuilderMock).register(MyResponseFilter1.class); + verify(restClientBuilderMock).queryParamStyle(COMMA_SEPARATED); + + verify(restClientBuilderMock).trustStore(Mockito.any(), Mockito.anyString()); + verify(restClientBuilderMock).keyStore(Mockito.any(), Mockito.anyString()); } @Test public void testGlobalConfigs() { // given - RestClientsConfig configRoot = createSampleConfigRoot(); + RestClientsConfig configRoot = ConfigUtils.emptyConfigBuilder() + .setAddDefaultSources(false) + .withMapping(RestClientsConfig.class) + .withDefaultValues(createSampleConfigRoot()) + .build() + .getConfigMapping(RestClientsConfig.class); // when @@ -141,107 +157,95 @@ public void testGlobalConfigs() { // then - Mockito.verify(restClientBuilderMock).baseUri(URI.create("http://localhost:8080")); - Mockito.verify(restClientBuilderMock) - .property(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE, PausableHttpPostRequestEncoder.EncoderMode.HTML5); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.DISABLE_CONTEXTUAL_ERROR_MESSAGES, true); - - Mockito.verify(restClientBuilderMock).proxyAddress("host2", 123); - Mockito.verify(restClientBuilderMock).proxyUser("proxyUser2"); - Mockito.verify(restClientBuilderMock).proxyPassword("proxyPassword2"); - Mockito.verify(restClientBuilderMock).nonProxyHosts("nonProxyHosts2"); - Mockito.verify(restClientBuilderMock).connectTimeout(200, TimeUnit.MILLISECONDS); - Mockito.verify(restClientBuilderMock).readTimeout(201, TimeUnit.MILLISECONDS); - Mockito.verify(restClientBuilderMock).userAgent("agent2"); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.STATIC_HEADERS, - Collections.singletonMap("header2", "value")); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.CONNECTION_TTL, 20); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.CONNECTION_POOL_SIZE, 203); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.KEEP_ALIVE_ENABLED, true); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.MAX_REDIRECTS, 204); - Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.MAX_CHUNK_SIZE, 1024); - Mockito.verify(restClientBuilderMock).followRedirects(true); - Mockito.verify(restClientBuilderMock).register(MyResponseFilter2.class); - Mockito.verify(restClientBuilderMock).queryParamStyle(QueryParamStyle.MULTI_PAIRS); - - Mockito.verify(restClientBuilderMock).trustStore(Mockito.any(), Mockito.anyString()); - Mockito.verify(restClientBuilderMock).keyStore(Mockito.any(), Mockito.anyString()); + verify(restClientBuilderMock).baseUri(URI.create("http://localhost:8080")); + verify(restClientBuilderMock).property(MULTIPART_ENCODER_MODE, HTML5); + verify(restClientBuilderMock).property(DISABLE_CONTEXTUAL_ERROR_MESSAGES, true); + + verify(restClientBuilderMock).proxyAddress("host2", 123); + verify(restClientBuilderMock).proxyUser("proxyUser2"); + verify(restClientBuilderMock).proxyPassword("proxyPassword2"); + verify(restClientBuilderMock).nonProxyHosts("nonProxyHosts2"); + verify(restClientBuilderMock).connectTimeout(200, MILLISECONDS); + verify(restClientBuilderMock).readTimeout(201, MILLISECONDS); + verify(restClientBuilderMock).userAgent("agent2"); + verify(restClientBuilderMock).property(STATIC_HEADERS, Map.of("header2", "value")); + verify(restClientBuilderMock).property(CONNECTION_TTL, 20); + verify(restClientBuilderMock).property(CONNECTION_POOL_SIZE, 203); + verify(restClientBuilderMock).property(KEEP_ALIVE_ENABLED, true); + verify(restClientBuilderMock).property(MAX_REDIRECTS, 204); + verify(restClientBuilderMock).property(MAX_CHUNK_SIZE, 1024); + verify(restClientBuilderMock).followRedirects(true); + verify(restClientBuilderMock).register(MyResponseFilter2.class); + verify(restClientBuilderMock).queryParamStyle(MULTI_PAIRS); + + verify(restClientBuilderMock).trustStore(Mockito.any(), Mockito.anyString()); + verify(restClientBuilderMock).keyStore(Mockito.any(), Mockito.anyString()); } - private static RestClientsConfig createSampleConfigRoot() { - RestClientsConfig configRoot = new RestClientsConfig(); - + private static Map createSampleConfigRoot() { + Map rootConfig = new HashMap<>(); // global properties: - configRoot.multipartPostEncoderMode = Optional.of("HTML5"); - configRoot.disableContextualErrorMessages = true; - + rootConfig.put("quarkus.rest-client.multipart-post-encoder-mode", "HTML5"); + rootConfig.put("quarkus.rest-client.disable-contextual-error-messages", "true"); // global defaults for client specific properties: - configRoot.proxyAddress = Optional.of("host2:123"); - configRoot.proxyUser = Optional.of("proxyUser2"); - configRoot.proxyPassword = Optional.of("proxyPassword2"); - configRoot.nonProxyHosts = Optional.of("nonProxyHosts2"); - configRoot.connectTimeout = 200L; - configRoot.readTimeout = 201L; - configRoot.userAgent = Optional.of("agent2"); - configRoot.headers = Collections.singletonMap("header2", "value"); - configRoot.connectionTTL = Optional.of(20000); // value in ms, will be converted to seconds - configRoot.connectionPoolSize = Optional.of(203); - configRoot.keepAliveEnabled = Optional.of(true); - configRoot.maxRedirects = Optional.of(204); - configRoot.multipart = new RestClientMultipartConfig(); - configRoot.multipart.maxChunkSize = Optional.of(1024); - configRoot.followRedirects = Optional.of(true); - configRoot.maxChunkSize = Optional.of(new MemorySize(BigInteger.valueOf(1024))); - configRoot.providers = Optional - .of("io.quarkus.rest.client.reactive.runtime.RestClientCDIDelegateBuilderTest$MyResponseFilter2"); - configRoot.queryParamStyle = Optional.of(QueryParamStyle.MULTI_PAIRS); - - configRoot.trustStore = Optional.of(truststorePath.toAbsolutePath().toString()); - configRoot.trustStorePassword = Optional.of("truststorePassword"); - configRoot.trustStoreType = Optional.of("JKS"); - configRoot.keyStore = Optional.of(keystorePath.toAbsolutePath().toString()); - configRoot.keyStorePassword = Optional.of("keystorePassword"); - configRoot.keyStoreType = Optional.of("JKS"); - - return configRoot; + rootConfig.put("quarkus.rest-client.proxy-address", "host2:123"); + rootConfig.put("quarkus.rest-client.proxy-user", "proxyUser2"); + rootConfig.put("quarkus.rest-client.proxy-password", "proxyPassword2"); + rootConfig.put("quarkus.rest-client.non-proxy-hosts", "nonProxyHosts2"); + rootConfig.put("quarkus.rest-client.connect-timeout", "200"); + rootConfig.put("quarkus.rest-client.read-timeout", "201"); + rootConfig.put("quarkus.rest-client.user-agent", "agent2"); + rootConfig.put("quarkus.rest-client.headers.header2", "value"); + rootConfig.put("quarkus.rest-client.connection-ttl", "20000"); + rootConfig.put("quarkus.rest-client.connection-pool-size", "203"); + rootConfig.put("quarkus.rest-client.keep-alive-enabled", "true"); + rootConfig.put("quarkus.rest-client.max-redirects", "204"); + rootConfig.put("quarkus.rest-client.multipart-max-chunk-size", "1024"); + rootConfig.put("quarkus.rest-client.follow-redirects", "true"); + rootConfig.put("quarkus.rest-client.max-chunk-size", "1024"); + rootConfig.put("quarkus.rest-client.providers", + "io.quarkus.rest.client.reactive.runtime.RestClientCDIDelegateBuilderTest$MyResponseFilter2"); + rootConfig.put("quarkus.rest-client.query-param-style", "multi-pairs"); + rootConfig.put("quarkus.rest-client.trust-store", truststorePath.toAbsolutePath().toString()); + rootConfig.put("quarkus.rest-client.trust-store-password", "truststorePassword"); + rootConfig.put("quarkus.rest-client.trust-store-type", "JKS"); + rootConfig.put("quarkus.rest-client.key-store", keystorePath.toAbsolutePath().toString()); + rootConfig.put("quarkus.rest-client.key-store-password", "keystorePassword"); + rootConfig.put("quarkus.rest-client.key-store-type", "JKS"); + return rootConfig; } - private static RestClientConfig createSampleClientConfig() { - RestClientConfig clientConfig = new RestClientConfig(); - + private static Map createSampleClientConfig(final String restClientName) { + Map clientConfig = new HashMap<>(); // properties only configurable via client config - clientConfig.url = Optional.of("http://localhost"); - clientConfig.uri = Optional.empty(); - clientConfig.shared = Optional.of(true); - clientConfig.name = Optional.of("my-client"); - + clientConfig.put("quarkus.rest-client." + restClientName + ".url", "http://localhost"); + clientConfig.put("quarkus.rest-client." + restClientName + ".uri", ""); + clientConfig.put("quarkus.rest-client." + restClientName + ".shared", "true"); + clientConfig.put("quarkus.rest-client." + restClientName + ".name", "my-client"); // properties that override configRoot counterparts - clientConfig.proxyAddress = Optional.of("host1:123"); - clientConfig.proxyUser = Optional.of("proxyUser1"); - clientConfig.proxyPassword = Optional.of("proxyPassword1"); - clientConfig.nonProxyHosts = Optional.of("nonProxyHosts1"); - clientConfig.connectTimeout = Optional.of(100L); - clientConfig.readTimeout = Optional.of(101L); - clientConfig.userAgent = Optional.of("agent1"); - clientConfig.headers = Collections.singletonMap("header1", "value"); - clientConfig.connectionTTL = Optional.of(10000); // value in milliseconds, will be converted to seconds - clientConfig.connectionPoolSize = Optional.of(103); - clientConfig.keepAliveEnabled = Optional.of(false); - clientConfig.maxRedirects = Optional.of(104); - clientConfig.followRedirects = Optional.of(true); - clientConfig.multipart = new RestClientMultipartConfig(); - clientConfig.maxChunkSize = Optional.of(new MemorySize(BigInteger.valueOf(1024))); - clientConfig.providers = Optional - .of("io.quarkus.rest.client.reactive.runtime.RestClientCDIDelegateBuilderTest$MyResponseFilter1"); - clientConfig.queryParamStyle = Optional.of(QueryParamStyle.COMMA_SEPARATED); - - clientConfig.trustStore = Optional.of(truststorePath.toAbsolutePath().toString()); - clientConfig.trustStorePassword = Optional.of("truststorePassword"); - clientConfig.trustStoreType = Optional.of("JKS"); - clientConfig.keyStore = Optional.of(keystorePath.toAbsolutePath().toString()); - clientConfig.keyStorePassword = Optional.of("keystorePassword"); - clientConfig.keyStoreType = Optional.of("JKS"); - + clientConfig.put("quarkus.rest-client." + restClientName + ".proxy-address", "host1:123"); + clientConfig.put("quarkus.rest-client." + restClientName + ".proxy-user", "proxyUser1"); + clientConfig.put("quarkus.rest-client." + restClientName + ".proxy-password", "proxyPassword1"); + clientConfig.put("quarkus.rest-client." + restClientName + ".non-proxy-hosts", "nonProxyHosts1"); + clientConfig.put("quarkus.rest-client." + restClientName + ".connect-timeout", "100"); + clientConfig.put("quarkus.rest-client." + restClientName + ".read-timeout", "101"); + clientConfig.put("quarkus.rest-client." + restClientName + ".user-agent", "agent1"); + clientConfig.put("quarkus.rest-client." + restClientName + ".headers.header1", "value"); + clientConfig.put("quarkus.rest-client." + restClientName + ".connection-ttl", "10000"); + clientConfig.put("quarkus.rest-client." + restClientName + ".connection-pool-size", "103"); + clientConfig.put("quarkus.rest-client." + restClientName + ".keep-alive-enabled", "false"); + clientConfig.put("quarkus.rest-client." + restClientName + ".max-redirects", "104"); + clientConfig.put("quarkus.rest-client." + restClientName + ".follow-redirects", "true"); + clientConfig.put("quarkus.rest-client." + restClientName + ".max-chunk-size", "1024"); + clientConfig.put("quarkus.rest-client." + restClientName + ".providers", + "io.quarkus.rest.client.reactive.runtime.RestClientCDIDelegateBuilderTest$MyResponseFilter1"); + clientConfig.put("quarkus.rest-client." + restClientName + ".query-param-style", "comma-separated"); + clientConfig.put("quarkus.rest-client." + restClientName + ".trust-store", truststorePath.toAbsolutePath().toString()); + clientConfig.put("quarkus.rest-client." + restClientName + ".trust-store-password", "truststorePassword"); + clientConfig.put("quarkus.rest-client." + restClientName + ".trust-store-type", "JKS"); + clientConfig.put("quarkus.rest-client." + restClientName + ".key-store", keystorePath.toAbsolutePath().toString()); + clientConfig.put("quarkus.rest-client." + restClientName + ".key-store-password", "keystorePassword"); + clientConfig.put("quarkus.rest-client." + restClientName + ".key-store-type", "JKS"); return clientConfig; } diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java index bd97894704a7fe..0f0f08e21ee551 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java +++ b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java @@ -25,6 +25,7 @@ import org.jboss.jandex.IndexView; import org.jboss.jandex.Indexer; import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type; import org.jboss.resteasy.reactive.common.core.BlockingNotAllowedException; import org.jboss.resteasy.reactive.common.model.ResourceContextResolver; import org.jboss.resteasy.reactive.common.model.ResourceExceptionMapper; @@ -33,6 +34,7 @@ import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; import org.jboss.resteasy.reactive.common.processor.scanning.ApplicationScanningResult; import org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveInterceptorScanner; +import org.jboss.resteasy.reactive.server.UnwrapException; import org.jboss.resteasy.reactive.server.core.ExceptionMapping; import org.jboss.resteasy.reactive.server.model.ContextResolvers; import org.jboss.resteasy.reactive.server.model.ParamConverterProviders; @@ -82,6 +84,9 @@ */ public class ResteasyReactiveScanningProcessor { + private static final DotName EXCEPTION = DotName.createSimple(Exception.class); + private static final DotName RUNTIME_EXCEPTION = DotName.createSimple(RuntimeException.class); + public static final Set CONDITIONAL_BEAN_ANNOTATIONS; static { @@ -118,11 +123,55 @@ public void accept(ResourceInterceptors interceptors) { } @BuildStep - public List defaultUnwrappedException() { + public List defaultUnwrappedExceptions() { return List.of(new UnwrappedExceptionBuildItem(ArcUndeclaredThrowableException.class), new UnwrappedExceptionBuildItem(RollbackException.class)); } + @BuildStep + public void applicationSpecificUnwrappedExceptions(CombinedIndexBuildItem combinedIndexBuildItem, + BuildProducer producer) { + IndexView index = combinedIndexBuildItem.getIndex(); + for (AnnotationInstance instance : index.getAnnotations(UnwrapException.class)) { + AnnotationValue value = instance.value(); + if (value == null) { + // in this case we need to use the class where the annotation was placed as the exception to be unwrapped + + AnnotationTarget target = instance.target(); + if (target.kind() != AnnotationTarget.Kind.CLASS) { + throw new IllegalStateException( + "@UnwrapException is only supported on classes. Offending target is: " + target); + } + ClassInfo classInfo = target.asClass(); + ClassInfo toCheck = classInfo; + boolean isException = false; + while (true) { + DotName superDotName = toCheck.superName(); + if (EXCEPTION.equals(superDotName) || RUNTIME_EXCEPTION.equals(superDotName)) { + isException = true; + break; + } + toCheck = index.getClassByName(superDotName); + if (toCheck == null) { + break; + } + } + if (!isException) { + throw new IllegalArgumentException( + "Using @UnwrapException without a value is only supported on exception classes. Offending target is '" + + classInfo.name() + "'."); + } + + producer.produce(new UnwrappedExceptionBuildItem(classInfo.name().toString())); + } else { + Type[] exceptionTypes = value.asClassArray(); + for (Type exceptionType : exceptionTypes) { + producer.produce(new UnwrappedExceptionBuildItem(exceptionType.name().toString())); + } + } + } + } + @BuildStep public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem combinedIndexBuildItem, ApplicationResultBuildItem applicationResultBuildItem, @@ -137,7 +186,7 @@ public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem exceptions.addBlockingProblem(BlockingOperationNotAllowedException.class); exceptions.addBlockingProblem(BlockingNotAllowedException.class); for (UnwrappedExceptionBuildItem bi : unwrappedExceptions) { - exceptions.addUnwrappedException(bi.getThrowableClass().getName()); + exceptions.addUnwrappedException(bi.getThrowableClassName()); } if (capabilities.isPresent(Capability.HIBERNATE_REACTIVE)) { exceptions.addNonBlockingProblem( diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/UnwrapExceptionTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/UnwrapExceptionTest.java new file mode 100644 index 00000000000000..c129c8a490d131 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/UnwrapExceptionTest.java @@ -0,0 +1,155 @@ +package io.quarkus.resteasy.reactive.server.test.customexceptions; + +import static io.quarkus.resteasy.reactive.server.test.ExceptionUtil.removeStackTrace; +import static io.restassured.RestAssured.when; + +import java.util.function.Supplier; + +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.jboss.resteasy.reactive.server.UnwrapException; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.resteasy.reactive.server.test.ExceptionUtil; +import io.quarkus.test.QuarkusUnitTest; + +public class UnwrapExceptionTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(FirstException.class, SecondException.class, ThirdException.class, + FourthException.class, FifthException.class, SixthException.class, + Mappers.class, Resource.class, ExceptionUtil.class); + } + }); + + @Test + public void testWrapperWithUnmappedException() { + when().get("/hello/iaeInSecond") + .then().statusCode(500); + } + + @Test + public void testMappedExceptionWithoutUnwrappedWrapper() { + when().get("/hello/iseInFirst") + .then().statusCode(500); + + when().get("/hello/iseInThird") + .then().statusCode(500); + + when().get("/hello/iseInSixth") + .then().statusCode(500); + } + + @Test + public void testWrapperWithMappedException() { + when().get("/hello/iseInSecond") + .then().statusCode(900); + + when().get("/hello/iseInFourth") + .then().statusCode(900); + + when().get("/hello/iseInFifth") + .then().statusCode(900); + } + + @Path("hello") + public static class Resource { + + @Path("iseInFirst") + public String throwsISEAsCauseOfFirstException() { + throw removeStackTrace(new FirstException(removeStackTrace(new IllegalStateException("dummy")))); + } + + @Path("iseInSecond") + public String throwsISEAsCauseOfSecondException() { + throw removeStackTrace(new SecondException(removeStackTrace(new IllegalStateException("dummy")))); + } + + @Path("iaeInSecond") + public String throwsIAEAsCauseOfSecondException() { + throw removeStackTrace(new SecondException(removeStackTrace(new IllegalArgumentException("dummy")))); + } + + @Path("iseInThird") + public String throwsISEAsCauseOfThirdException() throws ThirdException { + throw removeStackTrace(new ThirdException(removeStackTrace(new IllegalStateException("dummy")))); + } + + @Path("iseInFourth") + public String throwsISEAsCauseOfFourthException() throws FourthException { + throw removeStackTrace(new FourthException(removeStackTrace(new IllegalStateException("dummy")))); + } + + @Path("iseInFifth") + public String throwsISEAsCauseOfFifthException() { + throw removeStackTrace(new FifthException(removeStackTrace(new IllegalStateException("dummy")))); + } + + @Path("iseInSixth") + public String throwsISEAsCauseOfSixthException() { + throw removeStackTrace(new SixthException(removeStackTrace(new IllegalStateException("dummy")))); + } + } + + @UnwrapException({ FourthException.class, FifthException.class }) + public static class Mappers { + + @ServerExceptionMapper + public Response handleIllegalStateException(IllegalStateException e) { + return Response.status(900).build(); + } + } + + public static class FirstException extends RuntimeException { + + public FirstException(Throwable cause) { + super(cause); + } + } + + @UnwrapException + public static class SecondException extends FirstException { + + public SecondException(Throwable cause) { + super(cause); + } + } + + public static class ThirdException extends Exception { + + public ThirdException(Throwable cause) { + super(cause); + } + } + + public static class FourthException extends SecondException { + + public FourthException(Throwable cause) { + super(cause); + } + } + + public static class FifthException extends RuntimeException { + + public FifthException(Throwable cause) { + super(cause); + } + } + + public static class SixthException extends RuntimeException { + + public SixthException(Throwable cause) { + super(cause); + } + } +} diff --git a/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/UnwrappedExceptionBuildItem.java b/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/UnwrappedExceptionBuildItem.java index 6cb915916c3fab..9bf0b4dbfa59c1 100644 --- a/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/UnwrappedExceptionBuildItem.java +++ b/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/UnwrappedExceptionBuildItem.java @@ -3,18 +3,32 @@ import io.quarkus.builder.item.MultiBuildItem; /** - * When an Exception of this type is thrown and no {@code jakarta.ws.rs.ext.ExceptionMapper} exists, + * When an {@link Exception} of this type is thrown and no {@code jakarta.ws.rs.ext.ExceptionMapper} exists, * then RESTEasy Reactive will attempt to locate an {@code ExceptionMapper} for the cause of the Exception. */ public final class UnwrappedExceptionBuildItem extends MultiBuildItem { - private final Class throwableClass; + private final String throwableClassName; - public UnwrappedExceptionBuildItem(Class throwableClass) { - this.throwableClass = throwableClass; + public UnwrappedExceptionBuildItem(String throwableClassName) { + this.throwableClassName = throwableClassName; } + public UnwrappedExceptionBuildItem(Class throwableClassName) { + this.throwableClassName = throwableClassName.getName(); + } + + @Deprecated(forRemoval = true) public Class getThrowableClass() { - return throwableClass; + try { + return (Class) Class.forName(throwableClassName, false, + Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + public String getThrowableClassName() { + return throwableClassName; } } diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java index e13e64c127ae2a..2ab9e35c743446 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduled.java @@ -62,6 +62,28 @@ */ String DEFAULT_TIMEZONE = "<>"; + /** + * Constant value for {@link #executeWith()} indicating that the implementation should be selected automatically, i.e. the + * implementation with highest priority is used. + */ + String AUTO = "<>"; + + /** + * Constant value for {@link #executeWith()} indicating that the simple in-memory implementation provided by the + * {@code quarkus-scheduler} extension should be used. + *

    + * This implementation has priority {@code 0}. + */ + String SIMPLE = "SIMPLE"; + + /** + * Constant value for {@link #executeWith()} indicating that the Quartz implementation provided by the + * {@code quarkus-quartz} extension should be used. + *

    + * This implementation has priority {@code 1}. + */ + String QUARTZ = "QUARTZ"; + /** * Optionally defines a unique identifier for this job. *

    @@ -205,6 +227,30 @@ */ String timeZone() default DEFAULT_TIMEZONE; + /** + * Choose a scheduler implementation used to execute a scheduled method. + *

    + * Only one scheduler implementation is used for all scheduled methods by default. For example, the {@code quarkus-quartz} + * extension provides an implementation that supports clustering but it also removes the simple in-memory implementation + * from the game. + *

    + * If the {@code quarkus.scheduler.use-composite-scheduler} config property is set to {@code true} then a composite + * scheduler is used instead. This means that multiple scheduler implementations are kept running side by side. + * In this case, it's possible to choose a specific implementation used to execute a scheduled method. By default, the + * implementation with highest priority is selected automatically. + *

    + * If the {@code quarkus.scheduler.use-composite-scheduler} config property is set to {@code false} (default) and the + * required implementation is not the implementation with highest priority, then the build fails. + *

    + * In any case, if the required implementation is not available, then the build fails. + * + * @return the implementation to execute this scheduled method + * @see #AUTO + * @see #SIMPLE + * @see #QUARTZ + */ + String executeWith() default AUTO; + @Retention(RUNTIME) @Target(METHOD) @interface Schedules { diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java index f5dd65e7440c6a..2d10922bf9ab36 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Scheduler.java @@ -44,10 +44,14 @@ public interface Scheduler { /** * Identity must not be null and {@code false} is returned for non-existent identity. + *

    + * Note that this method only returns {@code true} if the job was explicitly paused. I.e. it does not reflect a paused + * scheduler. * * @param identity * @return {@code true} if the job with the given identity is paused, {@code false} otherwise * @see Scheduled#identity() + * @see #pause(String) */ boolean isPaused(String identity); @@ -88,6 +92,13 @@ public interface Scheduler { */ Trigger unscheduleJob(String identity); + /** + * + * @return the implementation + * @see Scheduled#executeWith() + */ + String implementation(); + /** * The job definition is a builder-like API that can be used to define a job programmatically. *

    @@ -177,6 +188,16 @@ interface JobDefinition { */ JobDefinition setTimeZone(String timeZone); + /** + * {@link Scheduled#executeWith()} + * + * @param implementation + * @return self + * @throws IllegalArgumentException If the composite scheduler is used and the selected implementation is not available + * @see Scheduled#executeWith() + */ + JobDefinition setExecuteWith(String implementation); + /** * * @param task diff --git a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java index eef7fe84d7c27a..3feb514cd413e0 100644 --- a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java +++ b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/AbstractJobDefinition.java @@ -1,5 +1,6 @@ package io.quarkus.scheduler.common.runtime; +import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; @@ -28,6 +29,7 @@ public abstract class AbstractJobDefinition implements JobDefinition { protected boolean scheduled = false; protected String timeZone = Scheduled.DEFAULT_TIMEZONE; protected boolean runOnVirtualThread; + protected String implementation = Scheduled.AUTO; public AbstractJobDefinition(String identity) { this.identity = identity; @@ -36,55 +38,63 @@ public AbstractJobDefinition(String identity) { @Override public JobDefinition setCron(String cron) { checkScheduled(); - this.cron = cron; + this.cron = Objects.requireNonNull(cron); return this; } @Override public JobDefinition setInterval(String every) { checkScheduled(); - this.every = every; + this.every = Objects.requireNonNull(every); return this; } @Override public JobDefinition setDelayed(String period) { checkScheduled(); - this.delayed = period; + this.delayed = Objects.requireNonNull(period); return this; } @Override public JobDefinition setConcurrentExecution(ConcurrentExecution concurrentExecution) { checkScheduled(); - this.concurrentExecution = concurrentExecution; + this.concurrentExecution = Objects.requireNonNull(concurrentExecution); return this; } @Override public JobDefinition setSkipPredicate(SkipPredicate skipPredicate) { checkScheduled(); - this.skipPredicate = skipPredicate; + this.skipPredicate = Objects.requireNonNull(skipPredicate); return this; } @Override public JobDefinition setSkipPredicate(Class skipPredicateClass) { - this.skipPredicateClass = skipPredicateClass; + checkScheduled(); + this.skipPredicateClass = Objects.requireNonNull(skipPredicateClass); return setSkipPredicate(SchedulerUtils.instantiateBeanOrClass(skipPredicateClass)); } @Override public JobDefinition setOverdueGracePeriod(String period) { checkScheduled(); - this.overdueGracePeriod = period; + this.overdueGracePeriod = Objects.requireNonNull(period); return this; } @Override public JobDefinition setTimeZone(String timeZone) { checkScheduled(); - this.timeZone = timeZone; + this.timeZone = Objects.requireNonNull(timeZone); + return this; + } + + @Override + public JobDefinition setExecuteWith(String implementation) { + checkScheduled(); + this.implementation = Objects.requireNonNull(implementation); return this; } @@ -94,14 +104,14 @@ public JobDefinition setTask(Consumer task, boolean runOnVir if (asyncTask != null) { throw new IllegalStateException("Async task was already set"); } - this.task = task; + this.task = Objects.requireNonNull(task); this.runOnVirtualThread = runOnVirtualThread; return this; } @Override public JobDefinition setTask(Class> taskClass, boolean runOnVirtualThread) { - this.taskClass = taskClass; + this.taskClass = Objects.requireNonNull(taskClass); return setTask(SchedulerUtils.instantiateBeanOrClass(taskClass), runOnVirtualThread); } @@ -111,13 +121,13 @@ public JobDefinition setAsyncTask(Function> asyncT if (task != null) { throw new IllegalStateException("Sync task was already set"); } - this.asyncTask = asyncTask; + this.asyncTask = Objects.requireNonNull(asyncTask); return this; } @Override public JobDefinition setAsyncTask(Class>> asyncTaskClass) { - this.asyncTaskClass = asyncTaskClass; + this.asyncTaskClass = Objects.requireNonNull(asyncTaskClass); return setAsyncTask(SchedulerUtils.instantiateBeanOrClass(asyncTaskClass)); } diff --git a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/SchedulerContext.java b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/SchedulerContext.java index 5b2a406467a26f..50cbe5387724f1 100644 --- a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/SchedulerContext.java +++ b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/SchedulerContext.java @@ -5,6 +5,8 @@ import com.cronutils.model.CronType; +import io.quarkus.scheduler.Scheduled; + public interface SchedulerContext { CronType getCronType(); @@ -13,6 +15,12 @@ public interface SchedulerContext { boolean forceSchedulerStart(); + List getScheduledMethods(String implementation); + + boolean matchesImplementation(Scheduled scheduled, String implementation); + + String autoImplementation(); + @SuppressWarnings("unchecked") default ScheduledInvoker createInvoker(String invokerClassName) { try { diff --git a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/SyntheticScheduled.java b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/SyntheticScheduled.java index a598ef744ad6ff..8345b061c925c8 100644 --- a/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/SyntheticScheduled.java +++ b/extensions/scheduler/common/src/main/java/io/quarkus/scheduler/common/runtime/SyntheticScheduled.java @@ -22,10 +22,11 @@ public final class SyntheticScheduled extends AnnotationLiteral imple private final ConcurrentExecution concurrentExecution; private final SkipPredicate skipPredicate; private final String timeZone; + private final String implementation; public SyntheticScheduled(String identity, String cron, String every, long delay, TimeUnit delayUnit, String delayed, String overdueGracePeriod, ConcurrentExecution concurrentExecution, - SkipPredicate skipPredicate, String timeZone) { + SkipPredicate skipPredicate, String timeZone, String implementation) { this.identity = Objects.requireNonNull(identity); this.cron = Objects.requireNonNull(cron); this.every = Objects.requireNonNull(every); @@ -36,6 +37,7 @@ public SyntheticScheduled(String identity, String cron, String every, long delay this.concurrentExecution = Objects.requireNonNull(concurrentExecution); this.skipPredicate = skipPredicate; this.timeZone = timeZone; + this.implementation = implementation; } @Override @@ -88,6 +90,11 @@ public String timeZone() { return timeZone; } + @Override + public String executeWith() { + return implementation; + } + public String toJson() { if (skipPredicate != null) { throw new IllegalStateException("A skipPredicate instance may not be serialized"); @@ -102,6 +109,7 @@ public String toJson() { json.put("overdueGracePeriod", overdueGracePeriod); json.put("concurrentExecution", concurrentExecution.toString()); json.put("timeZone", timeZone); + json.put("executeWith", implementation); return json.encode(); } @@ -110,7 +118,7 @@ public static SyntheticScheduled fromJson(String json) { return new SyntheticScheduled(jsonObj.getString("identity"), jsonObj.getString("cron"), jsonObj.getString("every"), jsonObj.getLong("delay"), TimeUnit.valueOf(jsonObj.getString("delayUnit")), jsonObj.getString("delayed"), jsonObj.getString("overdueGracePeriod"), ConcurrentExecution.valueOf(jsonObj.getString("concurrentExecution")), - null, jsonObj.getString("timeZone")); + null, jsonObj.getString("timeZone"), jsonObj.getString("executeWith")); } @Override diff --git a/extensions/scheduler/common/src/test/java/io/quarkus/scheduler/common/runtime/util/SchedulerUtilsTest.java b/extensions/scheduler/common/src/test/java/io/quarkus/scheduler/common/runtime/util/SchedulerUtilsTest.java index 09083fce197d24..493d4c10ea6025 100644 --- a/extensions/scheduler/common/src/test/java/io/quarkus/scheduler/common/runtime/util/SchedulerUtilsTest.java +++ b/extensions/scheduler/common/src/test/java/io/quarkus/scheduler/common/runtime/util/SchedulerUtilsTest.java @@ -134,6 +134,12 @@ public Class skipExecutionIf() { public String delayed() { return delayed; } + + @Override + public String executeWith() { + return AUTO; + } + }; } } diff --git a/extensions/scheduler/common/src/test/java/io/quarkus/scheduler/common/runtime/util/SyntheticScheduledTest.java b/extensions/scheduler/common/src/test/java/io/quarkus/scheduler/common/runtime/util/SyntheticScheduledTest.java index b80460db3b380a..a8119398ff8b3c 100644 --- a/extensions/scheduler/common/src/test/java/io/quarkus/scheduler/common/runtime/util/SyntheticScheduledTest.java +++ b/extensions/scheduler/common/src/test/java/io/quarkus/scheduler/common/runtime/util/SyntheticScheduledTest.java @@ -15,7 +15,7 @@ public class SyntheticScheduledTest { @Test public void testJson() { SyntheticScheduled s1 = new SyntheticScheduled("foo", "", "2s", 0, TimeUnit.SECONDS, "1s", "15m", - ConcurrentExecution.PROCEED, null, Scheduled.DEFAULT_TIMEZONE); + ConcurrentExecution.PROCEED, null, Scheduled.DEFAULT_TIMEZONE, Scheduled.AUTO); SyntheticScheduled s2 = SyntheticScheduled.fromJson(s1.toJson()); assertEquals(s1.identity(), s2.identity()); assertEquals(s1.concurrentExecution(), s2.concurrentExecution()); @@ -26,6 +26,7 @@ public void testJson() { assertEquals(s1.delayed(), s2.delayed()); assertEquals(s1.overdueGracePeriod(), s2.overdueGracePeriod()); assertEquals(s1.timeZone(), s2.timeZone()); + assertEquals(s1.executeWith(), s2.executeWith()); } } diff --git a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/DiscoveredImplementationsBuildItem.java b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/DiscoveredImplementationsBuildItem.java new file mode 100644 index 00000000000000..a9b6eda0f3f438 --- /dev/null +++ b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/DiscoveredImplementationsBuildItem.java @@ -0,0 +1,63 @@ +package io.quarkus.scheduler.deployment; + +import java.util.Objects; +import java.util.Set; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.scheduler.runtime.CompositeScheduler; +import io.quarkus.scheduler.runtime.Constituent; +import io.quarkus.scheduler.runtime.SchedulerConfig; +import io.smallrye.common.annotation.Identifier; + +/** + * This build item holds all discovered {@link io.quarkus.scheduler.Scheduler} implementations sorted by priority. Higher + * priority goes first. + */ +public final class DiscoveredImplementationsBuildItem extends SimpleBuildItem { + + private final String autoImplementation; + + private final Set implementations; + + private final boolean useCompositeScheduler; + + DiscoveredImplementationsBuildItem(String autoImplementation, Set implementations, boolean useCompositeScheduler) { + this.autoImplementation = Objects.requireNonNull(autoImplementation); + this.implementations = Objects.requireNonNull(implementations); + this.useCompositeScheduler = useCompositeScheduler; + } + + /** + * + * @return the implementation with highest priority + */ + public String getAutoImplementation() { + return autoImplementation; + } + + public Set getImplementations() { + return implementations; + } + + /** + * A composite scheduler is used if multiple scheduler implementations are found and + * {@link SchedulerConfig#useCompositeScheduler} is set to {@code true}. + *

    + * The extension will add: + *

      + *
    • the {@link Constituent} marker qualifier,
    • + *
    • the {@link Identifier} qualifier with the corresponding implementation value.
    • + *
    + * + * @return {@code true} if a composite scheduler is used + * @see CompositeScheduler + */ + public boolean isCompositeSchedulerUsed() { + return useCompositeScheduler && implementations.size() > 1; + } + + public boolean isAutoImplementation(String implementation) { + return autoImplementation.equals(implementation); + } + +} diff --git a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerImplementationBuildItem.java b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerImplementationBuildItem.java new file mode 100644 index 00000000000000..d135dd9eefa1e8 --- /dev/null +++ b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerImplementationBuildItem.java @@ -0,0 +1,52 @@ +package io.quarkus.scheduler.deployment; + +import org.jboss.jandex.DotName; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; + +/** + * An extension that provides an implementation of {@link Scheduler} must produce this build item. + *

    + * If multiple extensions produce this build item with the same {@link #implementation} value then the build fails. + */ +public final class SchedulerImplementationBuildItem extends MultiBuildItem { + + private final String implementation; + + private final DotName schedulerBeanClass; + + private final int priority; + + public SchedulerImplementationBuildItem(String implementation, DotName schedulerBeanClass, int priority) { + this.implementation = implementation; + this.schedulerBeanClass = schedulerBeanClass; + this.priority = priority; + } + + public String getImplementation() { + return implementation; + } + + public DotName getSchedulerBeanClass() { + return schedulerBeanClass; + } + + /** + * The implementation with highest priority is selected if {@link Scheduled#AUTO} is used. + * + * @return the priority + * @see Scheduled#AUTO + */ + public int getPriority() { + return priority; + } + + @Override + public String toString() { + return "SchedulerImplementationBuildItem [" + (implementation != null ? "implementation=" + implementation + ", " : "") + + "priority=" + priority + "]"; + } + +} diff --git a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java index 1188ec62004088..2db5a37c62741f 100644 --- a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java +++ b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java @@ -10,6 +10,7 @@ import java.time.Duration; import java.time.ZoneId; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -19,8 +20,10 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Function; +import java.util.stream.Collectors; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTransformation; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -82,9 +85,12 @@ import io.quarkus.scheduler.common.runtime.MutableScheduledMethod; import io.quarkus.scheduler.common.runtime.SchedulerContext; import io.quarkus.scheduler.common.runtime.util.SchedulerUtils; +import io.quarkus.scheduler.runtime.CompositeScheduler; +import io.quarkus.scheduler.runtime.Constituent; import io.quarkus.scheduler.runtime.SchedulerConfig; import io.quarkus.scheduler.runtime.SchedulerRecorder; import io.quarkus.scheduler.runtime.SimpleScheduler; +import io.smallrye.common.annotation.Identifier; public class SchedulerProcessor { @@ -97,9 +103,58 @@ public class SchedulerProcessor { static final String NESTED_SEPARATOR = "$_"; @BuildStep - void beans(Capabilities capabilities, BuildProducer additionalBeans) { + SchedulerImplementationBuildItem implementation() { + return new SchedulerImplementationBuildItem(Scheduled.SIMPLE, DotName.createSimple(SimpleScheduler.class), 0); + } + + @BuildStep + void compositeScheduler(SchedulerConfig config, List implementations, + BuildProducer additionalBeans, + BuildProducer discoveredImplementations) { + List sorted = implementations.stream() + .sorted(Comparator.comparingInt(SchedulerImplementationBuildItem::getPriority).reversed()).toList(); + Set found = sorted.stream().map(SchedulerImplementationBuildItem::getImplementation) + .collect(Collectors.toUnmodifiableSet()); + if (found.size() != implementations.size()) { + throw new IllegalStateException("Invalid scheduler implementations detected: " + implementations); + } + DiscoveredImplementationsBuildItem discovered = new DiscoveredImplementationsBuildItem( + sorted.get(0).getImplementation(), found, + config.useCompositeScheduler); + discoveredImplementations.produce(discovered); + if (implementations.size() > 1 && config.useCompositeScheduler) { + // If multiple implementations are needed we have to register the CompositeScheduler, and + // instruct the extensions that provide an implementation to modify the bean metadata, i.e. add the marker qualifier + additionalBeans.produce(AdditionalBeanBuildItem.builder() + .addBeanClasses(Constituent.class, CompositeScheduler.class).setUnremovable().build()); + } + } + + @BuildStep + void transformSchedulerBeans(DiscoveredImplementationsBuildItem discoveredImplementations, + List implementations, + BuildProducer transformer) { + if (discoveredImplementations.isCompositeSchedulerUsed()) { + Map implsToBeanClass = implementations.stream() + .collect(Collectors.toMap(SchedulerImplementationBuildItem::getSchedulerBeanClass, + SchedulerImplementationBuildItem::getImplementation)); + transformer.produce(new AnnotationsTransformerBuildItem(AnnotationTransformation.forClasses() + .whenClass(c -> implsToBeanClass.containsKey(c.name())) + .transform(c -> { + c.add(AnnotationInstance.builder(Constituent.class).build()); + c.add(AnnotationInstance.builder(Identifier.class) + .add("value", implsToBeanClass.get(c.declaration().asClass().name())).build()); + }))); + } + } + + @BuildStep + void beans(DiscoveredImplementationsBuildItem discoveredImplementations, + BuildProducer additionalBeans) { additionalBeans.produce(new AdditionalBeanBuildItem(Scheduled.ApplicationNotRunning.class)); - if (capabilities.isMissing(Capability.QUARTZ)) { + if (discoveredImplementations.getImplementations().size() == 1 + || discoveredImplementations.isCompositeSchedulerUsed()) { + // Quartz extension is not present or composite scheduler is used additionalBeans.produce(new AdditionalBeanBuildItem(SimpleScheduler.class)); } } @@ -195,7 +250,8 @@ private void collectScheduledMethods(IndexView index, TransformedAnnotationsBuil @BuildStep void validateScheduledBusinessMethods(SchedulerConfig config, List scheduledMethods, ValidationPhaseBuildItem validationPhase, BuildProducer validationErrors, - Capabilities capabilities, BeanArchiveIndexBuildItem beanArchiveIndex) { + Capabilities capabilities, BeanArchiveIndexBuildItem beanArchiveIndex, + DiscoveredImplementationsBuildItem discoveredImplementations) { List errors = new ArrayList<>(); Map encounteredIdentities = new HashMap<>(); Set methodDescriptions = new HashSet<>(); @@ -252,7 +308,7 @@ void validateScheduledBusinessMethods(SchedulerConfig config, List unremovableBeans() { public FeatureBuildItem build(SchedulerConfig config, BuildProducer syntheticBeans, SchedulerRecorder recorder, List scheduledMethods, BuildProducer generatedClasses, BuildProducer reflectiveClass, - AnnotationProxyBuildItem annotationProxy, List schedulerForcedStartItems) { + AnnotationProxyBuildItem annotationProxy, List schedulerForcedStartItems, + DiscoveredImplementationsBuildItem discoveredImplementations) { List scheduledMetadata = new ArrayList<>(); ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, new Function() { @@ -330,7 +387,8 @@ public String apply(String name) { } syntheticBeans.produce(SyntheticBeanBuildItem.configure(SchedulerContext.class).setRuntimeInit() - .supplier(recorder.createContext(config, scheduledMetadata, !schedulerForcedStartItems.isEmpty())) + .supplier(recorder.createContext(config, scheduledMetadata, !schedulerForcedStartItems.isEmpty(), + discoveredImplementations.getAutoImplementation())) .done()); return new FeatureBuildItem(Feature.SCHEDULER); @@ -530,8 +588,8 @@ private String generateInvoker(ScheduledBusinessMethodItem scheduledMethod, Clas } private Throwable validateScheduled(CronParser parser, AnnotationInstance schedule, - Map encounteredIdentities, - BeanDeploymentValidator.ValidationContext validationContext, long checkPeriod, IndexView index) { + Map encounteredIdentities, BeanDeploymentValidator.ValidationContext validationContext, + long checkPeriod, IndexView index, DiscoveredImplementationsBuildItem discoveredImplementations) { MethodInfo method = schedule.target().asMethod(); AnnotationValue cronValue = schedule.value("cron"); AnnotationValue everyValue = schedule.value("every"); @@ -645,6 +703,22 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu } } } + + AnnotationValue executeWithValue = schedule.value("executeWith"); + if (executeWithValue != null) { + String implementation = executeWithValue.asString(); + if (!Scheduled.AUTO.equals(implementation)) { + if (!discoveredImplementations.getImplementations().contains(implementation)) { + return new IllegalStateException( + "The required scheduler implementation was not discovered in application: " + implementation); + } else if (!discoveredImplementations.isCompositeSchedulerUsed() + && !discoveredImplementations.isAutoImplementation(implementation)) { + return new IllegalStateException( + "The required scheduler implementation is not available because the composite scheduler is not used: " + + implementation); + } + } + } return null; } diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/composite/SchedulerImplementationNotDiscoveredTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/composite/SchedulerImplementationNotDiscoveredTest.java new file mode 100644 index 00000000000000..045bfc8553cb44 --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/composite/SchedulerImplementationNotDiscoveredTest.java @@ -0,0 +1,36 @@ +package io.quarkus.scheduler.test.composite; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class SchedulerImplementationNotDiscoveredTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Jobs.class)) + .assertException(t -> { + assertThat(t).cause().isInstanceOf(IllegalStateException.class) + .hasMessageContaining( + "The required scheduler implementation was not discovered in application: QUARTZ"); + }); + + @Test + public void test() { + fail(); + } + + static class Jobs { + + @Scheduled(every = "1s", executeWith = Scheduled.QUARTZ) + void quartz() { + } + + } +} diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/CompositeScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/CompositeScheduler.java new file mode 100644 index 00000000000000..55d90854249bde --- /dev/null +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/CompositeScheduler.java @@ -0,0 +1,192 @@ +package io.quarkus.scheduler.runtime; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import jakarta.enterprise.inject.Typed; +import jakarta.inject.Singleton; + +import io.quarkus.arc.All; +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.scheduler.common.runtime.AbstractJobDefinition; +import io.quarkus.scheduler.common.runtime.SchedulerContext; + +/** + * The composite scheduler is only used in case of multiple {@link Scheduler} implementations are required. + * + * @see Scheduled#executeWith() + */ +@Typed(Scheduler.class) +@Singleton +public class CompositeScheduler implements Scheduler { + + private final List schedulers; + + private final SchedulerContext schedulerContext; + + CompositeScheduler(@All @Constituent List schedulers, SchedulerContext schedulerContext) { + this.schedulers = schedulers; + this.schedulerContext = schedulerContext; + } + + @Override + public void pause() { + for (Scheduler scheduler : schedulers) { + scheduler.pause(); + } + } + + @Override + public void pause(String identity) { + for (Scheduler scheduler : schedulers) { + scheduler.pause(identity); + } + } + + @Override + public void resume() { + for (Scheduler scheduler : schedulers) { + scheduler.resume(); + } + } + + @Override + public void resume(String identity) { + for (Scheduler scheduler : schedulers) { + scheduler.resume(identity); + } + } + + @Override + public boolean isPaused(String identity) { + for (Scheduler scheduler : schedulers) { + if (scheduler.isPaused(identity)) { + return true; + } + } + return false; + } + + @Override + public boolean isRunning() { + // IMPL NOTE: we return true if at least one of the schedulers is running + for (Scheduler scheduler : schedulers) { + if (scheduler.isRunning()) { + return true; + } + } + return false; + } + + @Override + public List getScheduledJobs() { + List triggers = new ArrayList<>(); + for (Scheduler scheduler : schedulers) { + triggers.addAll(scheduler.getScheduledJobs()); + } + return triggers; + } + + @Override + public Trigger getScheduledJob(String identity) { + for (Scheduler scheduler : schedulers) { + Trigger trigger = scheduler.getScheduledJob(identity); + if (trigger != null) { + return trigger; + } + } + return null; + } + + @Override + public JobDefinition newJob(String identity) { + return new CompositeJobDefinition(identity); + } + + @Override + public Trigger unscheduleJob(String identity) { + for (Scheduler scheduler : schedulers) { + Trigger trigger = scheduler.unscheduleJob(identity); + if (trigger != null) { + return trigger; + } + } + return null; + } + + @Override + public String implementation() { + return Scheduled.AUTO; + } + + class CompositeJobDefinition extends AbstractJobDefinition { + + public CompositeJobDefinition(String identity) { + super(identity); + } + + @Override + public JobDefinition setExecuteWith(String implementation) { + Objects.requireNonNull(implementation); + if (!Scheduled.AUTO.equals(implementation)) { + if (schedulers.stream().map(Scheduler::implementation).noneMatch(implementation::equals)) { + throw new IllegalArgumentException("Scheduler implementation not available: " + implementation); + } + } + return super.setExecuteWith(implementation); + } + + @Override + public Trigger schedule() { + String impl = implementation; + if (Scheduled.AUTO.equals(impl)) { + impl = schedulerContext.autoImplementation(); + } + for (Scheduler scheduler : schedulers) { + if (scheduler.implementation().equals(impl)) { + return copy(scheduler.newJob(identity)).schedule(); + } + } + throw new IllegalStateException("Matching scheduler implementation not found: " + implementation); + } + + private JobDefinition copy(JobDefinition to) { + to.setCron(cron); + to.setInterval(every); + to.setDelayed(delayed); + to.setOverdueGracePeriod(overdueGracePeriod); + to.setConcurrentExecution(concurrentExecution); + to.setTimeZone(timeZone); + to.setExecuteWith(implementation); + if (skipPredicateClass != null) { + to.setSkipPredicate(skipPredicateClass); + } else if (skipPredicate != null) { + to.setSkipPredicate(skipPredicate); + } + if (taskClass != null) { + if (runOnVirtualThread) { + to.setTask(taskClass, runOnVirtualThread); + } else { + to.setTask(taskClass); + } + } else if (task != null) { + if (runOnVirtualThread) { + to.setTask(task, runOnVirtualThread); + } else { + to.setTask(task); + } + } + if (asyncTaskClass != null) { + to.setAsyncTask(asyncTaskClass); + } else if (asyncTask != null) { + to.setAsyncTask(asyncTask); + } + return to; + } + + } + +} diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/Constituent.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/Constituent.java new file mode 100644 index 00000000000000..4fbf7c8a8d5dd1 --- /dev/null +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/Constituent.java @@ -0,0 +1,26 @@ +package io.quarkus.scheduler.runtime; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.inject.Qualifier; + +import io.quarkus.scheduler.Scheduler; + +/** + * This qualifier is used to mark a constituent of a composite {@link Scheduler}, i.e. to distinguish various scheduler + * implementations. + */ +@Qualifier +@Documented +@Retention(RUNTIME) +@Target({ TYPE, PARAMETER, FIELD }) +public @interface Constituent { + +} diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java index 23ce44a235145a..970ae9923e34b2 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerConfig.java @@ -6,6 +6,7 @@ import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; @ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) public class SchedulerConfig { @@ -30,4 +31,15 @@ public class SchedulerConfig { */ @ConfigItem(name = "tracing.enabled") public boolean tracingEnabled; + + /** + * By default, only one {@link Scheduler} implementation is used. If set to {@code true} then a composite {@link Scheduler} + * that delegates to all running implementations is used. + *

    + * Scheduler implementations will be started depending on the value of {@code quarkus.scheduler.start-mode}, i.e. the + * scheduler is not started unless a relevant {@link io.quarkus.scheduler.Scheduled} business method is found. + */ + @ConfigItem(defaultValue = "false") + public boolean useCompositeScheduler; + } diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerRecorder.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerRecorder.java index d0cc4037c9bc17..7198a7deb12b24 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerRecorder.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SchedulerRecorder.java @@ -7,6 +7,7 @@ import com.cronutils.model.CronType; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.scheduler.Scheduled; import io.quarkus.scheduler.common.runtime.ImmutableScheduledMethod; import io.quarkus.scheduler.common.runtime.MutableScheduledMethod; import io.quarkus.scheduler.common.runtime.ScheduledMethod; @@ -16,7 +17,7 @@ public class SchedulerRecorder { public Supplier createContext(SchedulerConfig config, - List scheduledMethods, boolean forceSchedulerStart) { + List scheduledMethods, boolean forceSchedulerStart, String autoImplementation) { // Defensive design - make an immutable copy of the scheduled method metadata List metadata = immutableCopy(scheduledMethods); return new Supplier() { @@ -38,6 +39,31 @@ public List getScheduledMethods() { public boolean forceSchedulerStart() { return forceSchedulerStart; } + + @Override + public List getScheduledMethods(String implementation) { + List ret = new ArrayList<>(metadata.size()); + for (ScheduledMethod method : metadata) { + for (Scheduled scheduled : method.getSchedules()) { + if (matchesImplementation(scheduled, implementation)) { + ret.add(method); + } + } + } + return ret; + } + + @Override + public boolean matchesImplementation(Scheduled scheduled, String implementation) { + return scheduled.executeWith().equals(implementation) || ((autoImplementation.equals(implementation)) + && scheduled.executeWith().equals(Scheduled.AUTO)); + } + + @Override + public String autoImplementation() { + return autoImplementation; + } + }; } }; diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java index 8a3d77797146e8..0a176bcaa2610b 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java @@ -131,7 +131,8 @@ public SimpleScheduler(SchedulerContext context, SchedulerRuntimeConfig schedule } StartMode startMode = schedulerRuntimeConfig.startMode.orElse(StartMode.NORMAL); - if (startMode == StartMode.NORMAL && context.getScheduledMethods().isEmpty() && !context.forceSchedulerStart()) { + if (startMode == StartMode.NORMAL && context.getScheduledMethods(Scheduled.SIMPLE).isEmpty() + && !context.forceSchedulerStart()) { this.scheduledExecutor = null; LOG.info("No scheduled business methods found - Simple scheduler will not be started"); return; @@ -168,9 +169,12 @@ public void run() { } // Create triggers and invokers for @Scheduled methods - for (ScheduledMethod method : context.getScheduledMethods()) { + for (ScheduledMethod method : context.getScheduledMethods(Scheduled.SIMPLE)) { int nameSequence = 0; for (Scheduled scheduled : method.getSchedules()) { + if (!context.matchesImplementation(scheduled, Scheduled.SIMPLE)) { + continue; + } nameSequence++; String id = SchedulerUtils.lookUpPropertyValue(scheduled.identity()); if (id.isEmpty()) { @@ -192,6 +196,11 @@ public void run() { } } + @Override + public String implementation() { + return Scheduled.SIMPLE; + } + @Override public JobDefinition newJob(String identity) { Objects.requireNonNull(identity); @@ -724,7 +733,7 @@ public boolean isBlocking() { }; } Scheduled scheduled = new SyntheticScheduled(identity, cron, every, 0, TimeUnit.MINUTES, delayed, - overdueGracePeriod, concurrentExecution, skipPredicate, timeZone); + overdueGracePeriod, concurrentExecution, skipPredicate, timeZone, implementation); Optional trigger = createTrigger(identity, null, cronParser, scheduled, defaultOverdueGracePeriod); if (trigger.isPresent()) { diff --git a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/AbstractMethodsAdder.java b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/AbstractMethodsAdder.java index 6033edfd7d6638..1ffedab3d40034 100644 --- a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/AbstractMethodsAdder.java +++ b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/AbstractMethodsAdder.java @@ -124,7 +124,7 @@ protected void generateFindQueryResultHandling(MethodCreator methodCreator, Resu if (customResultType == null) { ResultHandle casted = tryBlock.checkCast(singleResult, entityClassInfo.name().toString()); ResultHandle optional = tryBlock.invokeStaticMethod( - MethodDescriptor.ofMethod(Optional.class, "of", Optional.class, Object.class), + MethodDescriptor.ofMethod(Optional.class, "ofNullable", Optional.class, Object.class), casted); tryBlock.returnValue(optional); } else { @@ -134,7 +134,7 @@ protected void generateFindQueryResultHandling(MethodCreator methodCreator, Resu originalResultType), singleResult); ResultHandle optional = tryBlock.invokeStaticMethod( - MethodDescriptor.ofMethod(Optional.class, "of", Optional.class, Object.class), + MethodDescriptor.ofMethod(Optional.class, "ofNullable", Optional.class, Object.class), customResult); tryBlock.returnValue(optional); } diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BookListCrudRepository.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BookListCrudRepository.java index e429d089494992..b6def3e0addc71 100644 --- a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BookListCrudRepository.java +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BookListCrudRepository.java @@ -1,6 +1,10 @@ package io.quarkus.spring.data.deployment; +import java.util.Optional; + import org.springframework.data.repository.ListCrudRepository; public interface BookListCrudRepository extends ListCrudRepository { + + Optional findFirstByNameOrderByBid(String name); } diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BookListCrudRepositoryTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BookListCrudRepositoryTest.java index 5b1a6a39cff29f..f18b5cb2976d76 100644 --- a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BookListCrudRepositoryTest.java +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BookListCrudRepositoryTest.java @@ -69,6 +69,12 @@ public void shouldSaveBooks() { "Harry Potter and the Prisoner of Azkaban", "Harry Potter and the Globet of Fire"); } + @Test + @Transactional + public void optionalWithNonExisting() { + assertThat(repo.findFirstByNameOrderByBid("foobar")).isEmpty(); + } + private Book populateBook(Integer id, String title) { Book book = new Book(); book.setBid(id); diff --git a/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java b/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java index 2e14e0bf8e0150..240a96eb87f83b 100644 --- a/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java +++ b/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java @@ -14,6 +14,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -105,11 +106,13 @@ import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.deployment.logging.LoggingDecorateBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.deployment.util.ServiceUtil; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.logging.LogBuildTimeConfig; import io.quarkus.undertow.runtime.HttpSessionContext; import io.quarkus.undertow.runtime.QuarkusIdentityManager; import io.quarkus.undertow.runtime.ServletHttpSecurityPolicy; @@ -392,6 +395,8 @@ public ServletDeploymentManagerBuildItem build(List servlets, LaunchModeBuildItem launchMode, ShutdownContextBuildItem shutdownContext, KnownPathsBuildItem knownPaths, + LogBuildTimeConfig logBuildTimeConfig, + Optional loggingDecorateBuildItem, HttpBuildTimeConfig httpBuildTimeConfig, HttpRootPathBuildItem httpRootPath, ServletConfig servletConfig, @@ -682,8 +687,21 @@ public ServletDeploymentManagerBuildItem build(List servlets, } } + String scrMainJava = null; + List knownClasses = null; + if (loggingDecorateBuildItem.isPresent()) { + scrMainJava = loggingDecorateBuildItem.get().getSrcMainJava().toString(); + knownClasses = loggingDecorateBuildItem.get().getKnowClasses(); + } + return new ServletDeploymentManagerBuildItem( - recorder.bootServletContainer(deployment, bc.getValue(), launchMode.getLaunchMode(), shutdownContext)); + recorder.bootServletContainer(deployment, + bc.getValue(), + launchMode.getLaunchMode(), + shutdownContext, + logBuildTimeConfig.decorateStacktraces, + scrMainJava, + knownClasses)); } diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java index 19f63cc1bf6f5c..798475c142eacd 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java @@ -4,6 +4,9 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; @@ -16,6 +19,9 @@ public class QuarkusErrorServlet extends HttpServlet { public static final String SHOW_STACK = "show-stack"; + public static final String SHOW_DECORATION = "show-decoration"; + public static final String SRC_MAIN_JAVA = "src-main-java"; + public static final String KNOWN_CLASSES = "known-classes"; @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { @@ -53,6 +59,16 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws if (showStack && exception != null) { htmlBuilder.stack(exception); } + final boolean showDecoration = Boolean.parseBoolean(getInitParameter(SHOW_DECORATION)); + final String srcMainJava = getInitParameter(SRC_MAIN_JAVA); + final String knownClassesString = getInitParameter(KNOWN_CLASSES); + List knownClasses = null; + if (knownClassesString != null) { + knownClasses = new ArrayList<>(Arrays.asList(knownClassesString.split(","))); + } + if (showDecoration && exception != null && srcMainJava != null && knownClasses != null) { + htmlBuilder.decorate(exception, srcMainJava, knownClasses); + } resp.getWriter().write(htmlBuilder.toString()); } } diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java index fb00e1a9bd6cb2..5b125f47f90da3 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java @@ -459,7 +459,8 @@ public Supplier servletContextSupplier() { } public DeploymentManager bootServletContainer(RuntimeValue info, BeanContainer beanContainer, - LaunchMode launchMode, ShutdownContext shutdownContext) { + LaunchMode launchMode, ShutdownContext shutdownContext, boolean decorateStacktrace, String scrMainJava, + List knownClasses) { if (info.getValue().getExceptionHandler() == null) { //if a 500 error page has not been mapped we change the default to our more modern one, with a UID in the //log. If this is not production we also include the stack trace @@ -475,9 +476,16 @@ public DeploymentManager bootServletContainer(RuntimeValue info, if (!alreadyMapped500 || launchMode.isDevOrTest()) { info.getValue().setExceptionHandler(new QuarkusExceptionHandler()); info.getValue().addErrorPage(new ErrorPage("/@QuarkusError", StatusCodes.INTERNAL_SERVER_ERROR)); + String knownClassesString = null; + if (knownClasses != null) + knownClassesString = String.join(",", knownClasses); info.getValue().addServlet(new ServletInfo("@QuarkusError", QuarkusErrorServlet.class) .addMapping("/@QuarkusError").setAsyncSupported(true) - .addInitParam(QuarkusErrorServlet.SHOW_STACK, Boolean.toString(launchMode.isDevOrTest()))); + .addInitParam(QuarkusErrorServlet.SHOW_STACK, Boolean.toString(launchMode.isDevOrTest())) + .addInitParam(QuarkusErrorServlet.SHOW_DECORATION, + Boolean.toString(decorateStacktrace(launchMode, decorateStacktrace))) + .addInitParam(QuarkusErrorServlet.SRC_MAIN_JAVA, scrMainJava) + .addInitParam(QuarkusErrorServlet.KNOWN_CLASSES, knownClassesString)); } if (!alreadyMapped404 && launchMode.equals(LaunchMode.DEVELOPMENT)) { info.getValue().addErrorPage(new ErrorPage("/@QuarkusNotFound", StatusCodes.NOT_FOUND)); @@ -764,6 +772,10 @@ public void addErrorPage(RuntimeValue deployment, String locatio deployment.getValue().addErrorPage(new ErrorPage(location, exceptionType)); } + private boolean decorateStacktrace(LaunchMode launchMode, boolean decorateStacktrace) { + return decorateStacktrace && launchMode.equals(LaunchMode.DEVELOPMENT); + } + /** * we can't have SecureRandom in the native image heap, so we need to lazy init */ diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java index 0f772ff344f08e..ffee04566e460e 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java @@ -15,12 +15,16 @@ import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.ide.EffectiveIdeBuildItem; import io.quarkus.deployment.ide.Ide; import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem; import io.quarkus.utilities.OS; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.runtime.ide.IdeRecorder; /** * Processor for Ide interaction in Dev UI @@ -30,12 +34,23 @@ public class IdeProcessor { private static final Map LANG_TO_EXT = Map.of("java", "java", "kotlin", "kt"); @BuildStep(onlyIf = IsDevelopment.class) - void createJsonRPCService(BuildProducer buildTimeActionProducer, - Optional effectiveIdeBuildItem) { + @Record(ExecutionTime.RUNTIME_INIT) + void createOpenInIDEService(BuildProducer buildTimeActionProducer, + BuildProducer routeProducer, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + Optional effectiveIdeBuildItem, + IdeRecorder recorder) { if (effectiveIdeBuildItem.isPresent()) { Ide ide = effectiveIdeBuildItem.get().getIde(); if (ide != null) { + // For normal links (like from the error page) + routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route("open-in-ide/:fileName/:lang/:lineNumber") + .handler(recorder.openInIde()) + .build()); + + // For Dev UI (like from the server log) BuildTimeActionBuildItem ideActions = new BuildTimeActionBuildItem(NAMESPACE); ideActions.addAction("open", map -> { diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java index 0f8d232660a8d3..f02835a6ffae8b 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java @@ -13,6 +13,7 @@ import io.quarkus.deployment.builditem.StreamingLogHandlerBuildItem; import io.quarkus.deployment.dev.RuntimeUpdatesProcessor; import io.quarkus.deployment.dev.testing.TestSupport; +import io.quarkus.deployment.logging.LoggingDecorateBuildItem; import io.quarkus.dev.spi.DevModeType; import io.quarkus.devui.runtime.logstream.LogStreamBroadcaster; import io.quarkus.devui.runtime.logstream.LogStreamJsonRPCService; @@ -21,6 +22,7 @@ import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.logging.LogBuildTimeConfig; /** * Processor for Log stream in Dev UI @@ -40,8 +42,12 @@ void additionalBean(BuildProducer additionalBeanProduce @Record(ExecutionTime.STATIC_INIT) @SuppressWarnings("unchecked") public void handler(BuildProducer streamingLogHandlerBuildItem, + LogBuildTimeConfig logBuildTimeConfig, + LoggingDecorateBuildItem loggingDecorateBuildItem, LogStreamRecorder recorder) { - RuntimeValue> mutinyLogHandler = recorder.mutinyLogHandler(); + RuntimeValue> mutinyLogHandler = recorder.mutinyLogHandler( + logBuildTimeConfig.decorateStacktraces, loggingDecorateBuildItem.getSrcMainJava().toString(), + loggingDecorateBuildItem.getKnowClasses()); streamingLogHandlerBuildItem.produce(new StreamingLogHandlerBuildItem((RuntimeValue) mutinyLogHandler)); } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index 33f9771cf88370..c5c9a6c8021ea5 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -45,6 +45,7 @@ import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; +import io.quarkus.deployment.logging.LoggingDecorateBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageRunnerBuildItem; import io.quarkus.deployment.pkg.steps.GraalVM; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; @@ -55,6 +56,7 @@ import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.LiveReloadConfig; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.logging.LogBuildTimeConfig; import io.quarkus.runtime.shutdown.ShutdownConfig; import io.quarkus.tls.TlsRegistryBuildItem; import io.quarkus.utilities.OS; @@ -327,7 +329,8 @@ BodyHandlerBuildItem bodyHandler(VertxHttpRecorder recorder) { @BuildStep @Record(ExecutionTime.RUNTIME_INIT) - ServiceStartBuildItem finalizeRouter( + ServiceStartBuildItem finalizeRouter(Optional decorateBuildItem, + LogBuildTimeConfig logBuildTimeConfig, VertxHttpRecorder recorder, BeanContainerBuildItem beanContainer, CoreVertxBuildItem vertx, LaunchModeBuildItem launchMode, List defaultRoutes, @@ -399,6 +402,13 @@ ServiceStartBuildItem finalizeRouter( combinedActions.addAll(errorPageActionsBuildItem.getActions()); } + String srcMainJava = null; + List knowClasses = null; + if (decorateBuildItem.isPresent()) { + srcMainJava = decorateBuildItem.get().getSrcMainJava().toString(); + knowClasses = decorateBuildItem.get().getKnowClasses(); + } + recorder.finalizeRouter(beanContainer.getValue(), defaultRoute.map(DefaultRouteBuildItem::getRoute).orElse(null), listOfFilters, listOfManagementInterfaceFilters, @@ -410,6 +420,9 @@ ServiceStartBuildItem finalizeRouter( launchMode.getLaunchMode(), getBodyHandlerRequiredConditions(requireBodyHandlerBuildItems), bodyHandlerBuildItem.getHandler(), gracefulShutdownFilter, shutdownConfig, executorBuildItem.getExecutorProxy(), + logBuildTimeConfig, + srcMainJava, + knowClasses, combinedActions); return new ServiceStartBuildItem("vertx-http"); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java index 698376e023e141..723d445015074f 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java @@ -242,7 +242,8 @@ private Matcher htmlBodyMatcher() { containsString(""), containsString("Internal Server Error"), containsString("java.lang.RuntimeException: Simulated failure"), - containsString("at " + BeanRegisteringRouteThatThrowsException.class.getName() + "$1.handle")); + containsString(BeanRegisteringRouteThatThrowsException.class.getName() + "$1"), + containsString(".handle")); } @ApplicationScoped diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js index 838189ff4e6dfc..1fc0bfca87d2a8 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js @@ -262,7 +262,7 @@ export class QwcServerLog extends QwcAbstractLogElement { ${this._renderProcessName(message.processName)} ${this._renderThreadId(message.threadId)} ${this._renderThreadName(message.threadName)} - ${this._renderMessage(level, message.formattedMessage, message.stacktrace)} + ${this._renderMessage(level, message.formattedMessage, message.stacktrace, message.decoration)} `; } } @@ -403,7 +403,7 @@ export class QwcServerLog extends QwcAbstractLogElement { } } - _renderMessage(level, message, stacktrace){ + _renderMessage(level, message, stacktrace, decoration){ if(this._selectedColumns.includes('19')){ // Clean up Ansi message = message.replace(/\u001b\[.*?m/g, ""); @@ -417,27 +417,37 @@ export class QwcServerLog extends QwcAbstractLogElement { } // Make sure multi line is supported - if(message.includes('\n')){ - var htmlifiedLines = []; - var lines = message.split('\n'); - for (var i = 0; i < lines.length; i++) { - var line = lines[i]; - line = line.replace(/ /g, '\u00a0'); - if(i === lines.length-1){ - htmlifiedLines.push(line); - }else{ - htmlifiedLines.push(line + '<br/>'); - } - } - message = htmlifiedLines.join(''); - } + message = this._makeMultiLine(message); if(message){ - return html`<span title="Message" class='text-${level}'>${unsafeHTML(message)}${this._renderStackTrace(stacktrace)}</span>`; + return html`<span title="Message" class='text-${level}'>${unsafeHTML(message)}${this._renderDecoration(decoration)}${this._renderStackTrace(stacktrace)}</span>`; + } + } + } + + _renderDecoration(decoration){ + if(decoration){ + decoration = this._makeMultiLine("\n" + decoration + "\n"); + return html`${unsafeHTML(decoration)}`; + } + } + + _makeMultiLine(message){ + if(message.includes('\n')){ + var htmlifiedLines = []; + var lines = message.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + line = line.replace(/ /g, '\u00a0'); + if(i === lines.length-1){ + htmlifiedLines.push(line); + }else{ + htmlifiedLines.push(line + '<br/>'); + } } - - + message = htmlifiedLines.join(''); } + return message; } _renderStackTrace(stacktrace){ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java index 989be2fc5ef06a..ffb79a3a19a48f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java @@ -1,5 +1,6 @@ package io.quarkus.devui.runtime.logstream; +import java.util.List; import java.util.Optional; import io.quarkus.runtime.RuntimeValue; @@ -8,8 +9,9 @@ @Recorder public class LogStreamRecorder { - public RuntimeValue<Optional<MutinyLogHandler>> mutinyLogHandler() { - return new RuntimeValue<>(Optional.of(new MutinyLogHandler())); + public RuntimeValue<Optional<MutinyLogHandler>> mutinyLogHandler(boolean decorateStack, String srcMainJava, + List<String> knownClasses) { + return new RuntimeValue<>(Optional.of(new MutinyLogHandler(decorateStack, srcMainJava, knownClasses))); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java index dbe414c9db0de4..9454c04fa316b5 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java @@ -1,9 +1,12 @@ package io.quarkus.devui.runtime.logstream; +import java.util.List; + import org.jboss.logmanager.ExtHandler; import org.jboss.logmanager.ExtLogRecord; import io.quarkus.arc.Arc; +import io.quarkus.runtime.logging.DecorateStackUtil; import io.vertx.core.json.JsonObject; /** @@ -12,8 +15,14 @@ public class MutinyLogHandler extends ExtHandler { private LogStreamBroadcaster logStreamBroadcaster; - - public MutinyLogHandler() { + private final boolean decorateStack; + private final String srcMainJava; + private final List<String> knownClasses; + + public MutinyLogHandler(boolean decorateStack, String srcMainJava, List<String> knownClasses) { + this.decorateStack = decorateStack; + this.srcMainJava = srcMainJava; + this.knownClasses = knownClasses; setFormatter(new JsonFormatter()); } @@ -28,6 +37,14 @@ public final void doPublish(final ExtLogRecord record) { LogStreamBroadcaster broadcaster = getBroadcaster(); if (broadcaster != null) { JsonObject message = ((JsonFormatter) getFormatter()).toJsonObject(record); + if (decorateStack) { + String decoratedString = DecorateStackUtil.getDecoratedString(record.getThrown(), this.srcMainJava, + knownClasses); + if (decoratedString != null) { + message.put("decoration", decoratedString); + } + } + broadcaster.onNext(message); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/DecoratedAssertionError.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/DecoratedAssertionError.java deleted file mode 100644 index 85848a9949aba5..00000000000000 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/DecoratedAssertionError.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.quarkus.vertx.http.runtime; - -import java.io.PrintStream; -import java.io.PrintWriter; - -/** - * Wraps an exception and prints stack trace with code snippets. - * - * <p> - * To obtain the original exception, use {@link #getOriginal()}. - * - * <p> - * NOTE: Copied from <a href="https://github.com/laech/java-stacksrc">laech/java-stacksrc</a> - */ -final class DecoratedAssertionError extends AssertionError { - - private final Throwable original; - private final String decoratedStackTrace; - - DecoratedAssertionError(Throwable original) { - this(original, null); - } - - /** - * @param pruneStackTraceKeepFromClass if not null, will prune the stack traces, keeping only - * elements that are called directly or indirectly by this class - */ - DecoratedAssertionError( - Throwable original, Class<?> pruneStackTraceKeepFromClass) { - this.original = original; - this.decoratedStackTrace = StackTraceDecorator.get().decorate(original, pruneStackTraceKeepFromClass); - setStackTrace(new StackTraceElement[0]); - } - - @Override - public String getMessage() { - // Override this instead of calling the super(message) constructor, as super(null) will create - // the "null" string instead of actually being null - return getOriginal().getMessage(); - } - - /** Gets the original throwable being wrapped. */ - public Throwable getOriginal() { - return original; - } - - @Override - public void printStackTrace(PrintWriter out) { - out.println(this); - } - - @Override - public void printStackTrace(PrintStream out) { - out.println(this); - } - - @Override - public String toString() { - return decoratedStackTrace; - } -} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FileCollector.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FileCollector.java deleted file mode 100644 index 8fb5e5bd6dbce5..00000000000000 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FileCollector.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.quarkus.vertx.http.runtime; - -import java.io.IOException; -import java.nio.file.FileVisitOption; -import java.nio.file.FileVisitResult; -import java.nio.file.FileVisitor; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * NOTE: Copied from <a href="https://github.com/laech/java-stacksrc">laech/java-stacksrc</a> - */ -final class FileCollector implements FileVisitor<Path> { - - private final Map<String, List<Path>> result = new HashMap<>(); - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - result.computeIfAbsent(file.getFileName().toString(), __ -> new ArrayList<>()).add(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFileFailed(Path file, IOException exc) { - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) { - return FileVisitResult.CONTINUE; - } - - static Map<String, List<Path>> collect(Path dir) throws IOException { - var collector = new FileCollector(); - Files.walkFileTree(dir, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, collector); - return collector.result; - } -} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 2e581bcb613d9a..8a5465f671650d 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -289,12 +289,6 @@ public class HttpConfiguration { @ConfigItem public Map<String, FilterConfig> filter; - /** - * This will decorate the stacktrace in dev mode to show the line in the code that cause the exception - */ - @ConfigItem(defaultValue = "true") - public Boolean decorateStacktraces; - public ProxyConfig proxy; public int determinePort(LaunchMode launchMode) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java index 8405fe52bfe00d..7e7021abd8ecfd 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java @@ -45,18 +45,24 @@ public class QuarkusErrorHandler implements Handler<RoutingContext> { private final boolean decorateStack; private final Optional<HttpConfiguration.PayloadHint> contentTypeDefault; private final List<ErrorPageAction> actions; + private final List<String> knowClasses; + private final String srcMainJava; public QuarkusErrorHandler(boolean showStack, boolean decorateStack, Optional<HttpConfiguration.PayloadHint> contentTypeDefault) { - this(showStack, decorateStack, contentTypeDefault, List.of()); + this(showStack, decorateStack, contentTypeDefault, null, List.of(), List.of()); } public QuarkusErrorHandler(boolean showStack, boolean decorateStack, Optional<HttpConfiguration.PayloadHint> contentTypeDefault, + String srcMainJava, + List<String> knowClasses, List<ErrorPageAction> actions) { this.showStack = showStack; this.decorateStack = decorateStack; this.contentTypeDefault = contentTypeDefault; + this.srcMainJava = srcMainJava; + this.knowClasses = knowClasses; this.actions = actions; } @@ -141,9 +147,6 @@ public void accept(Throwable throwable) { exception.addSuppressed(e); } if (showStack && exception != null) { - if (decorateStack) { - exception = new DecoratedAssertionError(exception); - } details = generateHeaderMessage(exception, uuid); stack = generateStackTrace(exception); } else { @@ -211,8 +214,12 @@ private void htmlResponse(RoutingContext event, String details, Throwable except event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8"); final TemplateHtmlBuilder htmlBuilder = new TemplateHtmlBuilder("Internal Server Error", details, details, this.actions); + + if (decorateStack && exception != null) { + htmlBuilder.decorate(exception, this.srcMainJava, this.knowClasses); + } if (showStack && exception != null) { - htmlBuilder.stack(exception); + htmlBuilder.stack(exception, this.knowClasses); } writeResponse(event, htmlBuilder.toString()); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StackTraceDecorator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StackTraceDecorator.java deleted file mode 100644 index 2f0a1a4b32c375..00000000000000 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StackTraceDecorator.java +++ /dev/null @@ -1,235 +0,0 @@ -package io.quarkus.vertx.http.runtime; - -import static java.lang.String.format; -import static java.lang.System.lineSeparator; -import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.NavigableMap; -import java.util.Optional; -import java.util.Set; -import java.util.TreeMap; -import java.util.stream.IntStream; - -/** - * <p> - * NOTE: Copied from <a href="https://github.com/laech/java-stacksrc">laech/java-stacksrc</a> - */ -final class StackTraceDecorator { - private StackTraceDecorator() { - } - - private static final StackTraceDecorator instance = new StackTraceDecorator(); - - public static StackTraceDecorator get() { - return instance; - } - - private static final int CONTEXT_LINE_COUNT = 2; - - private volatile Map<String, List<Path>> cachedFiles; - - private Map<String, List<Path>> cachedFiles() throws IOException { - if (cachedFiles == null) { - cachedFiles = FileCollector.collect(Paths.get("")); - } - return cachedFiles; - } - - public String decorate(Throwable e) { - return decorate(e, null); - } - - public String decorate(Throwable e, Class<?> keepFromClass) { - if (keepFromClass != null) { - pruneStackTrace(e, keepFromClass, new HashSet<>()); - } - - var stackTrace = getStackTraceAsString(e); - try { - - var alreadySeenElements = new HashSet<StackTraceElement>(); - var alreadySeenSnippets = new HashSet<List<String>>(); - stackTrace = decorate(e, stackTrace, 1, alreadySeenElements, alreadySeenSnippets); - - var cause = e.getCause(); - if (cause != null) { - stackTrace = decorate(cause, stackTrace, 1, alreadySeenElements, alreadySeenSnippets); - } - - for (var suppressed : e.getSuppressed()) { - stackTrace = decorate(suppressed, stackTrace, 2, alreadySeenElements, alreadySeenSnippets); - } - - } catch (Exception sup) { - e.addSuppressed(sup); - } - return stackTrace; - } - - private String decorate( - Throwable e, - String stackTrace, - int indentLevel, - Set<StackTraceElement> alreadySeenElements, - Set<List<String>> alreadySeenSnippets) - throws IOException { - - for (var element : e.getStackTrace()) { - if (!alreadySeenElements.add(element)) { - continue; - } - - var snippet = decorate(element); - if (snippet.isEmpty() || !alreadySeenSnippets.add(snippet.get())) { - // Don't print the same snippet multiple times, - // multiple lambda on one line can create this situation - continue; - } - - var line = element.toString(); - var indent = "\t".repeat(indentLevel); - var replacement = String.format( - "%s%n%n%s%n%n", - line, snippet.get().stream().collect(joining(lineSeparator() + indent, indent, ""))); - stackTrace = stackTrace.replace(line, replacement); - } - return stackTrace; - } - - private Optional<List<String>> decorate(StackTraceElement element) throws IOException { - var file = findFile(element); - if (file.isEmpty()) { - return Optional.empty(); - } - - var lines = readContextLines(element, file.get()); - if (lines.isEmpty()) { - return Optional.empty(); - } - - removeBlankLinesFromStart(lines); - removeBlankLinesFromEnd(lines); - return Optional.of(buildSnippet(lines, element)); - } - - private Optional<Path> findFile(StackTraceElement element) throws IOException { - if (element.getLineNumber() < 1 - || element.getFileName() == null - || element.getMethodName().startsWith("access$")) { // Ignore class entry lines - return Optional.empty(); - } - - var tail = withPackagePath(element); - var paths = cachedFiles().getOrDefault(element.getFileName(), List.of()); - var exact = paths.stream().filter(it -> it.endsWith(tail)).findAny(); - if (exact.isPresent() || element.getFileName().endsWith(".java")) { - return exact; - } - return Optional.ofNullable(paths.size() == 1 ? paths.get(0) : null); - } - - private Path withPackagePath(StackTraceElement element) { - var fileName = requireNonNull(element.getFileName()); - var className = element.getClassName(); - var i = className.lastIndexOf("."); - var parent = i < 0 ? "" : className.substring(0, i).replace('.', '/'); - return Paths.get(parent).resolve(fileName); - } - - private NavigableMap<Integer, String> readContextLines(StackTraceElement elem, Path path) - throws IOException { - - var startLineNum = Math.max(1, elem.getLineNumber() - CONTEXT_LINE_COUNT); - try (var stream = Files.lines(path)) { - - var lines = stream - .limit(elem.getLineNumber() + CONTEXT_LINE_COUNT) - .skip(startLineNum - 1) - .collect(toList()); - - return IntStream.range(0, lines.size()) - .boxed() - .reduce( - new TreeMap<>(), - (acc, i) -> { - acc.put(i + startLineNum, lines.get(i)); - return acc; - }, - (a, b) -> b); - } - } - - @SuppressWarnings("NullAway") - private void removeBlankLinesFromStart(NavigableMap<Integer, String> lines) { - IntStream.rangeClosed(lines.firstKey(), lines.lastKey()) - .takeWhile(i -> lines.get(i).isBlank()) - .forEach(lines::remove); - } - - @SuppressWarnings("NullAway") - private void removeBlankLinesFromEnd(NavigableMap<Integer, String> lines) { - IntStream.iterate(lines.lastKey(), i -> i >= lines.firstKey(), i -> i - 1) - .takeWhile(i -> lines.get(i).isBlank()) - .forEach(lines::remove); - } - - private static List<String> buildSnippet( - NavigableMap<Integer, String> lines, StackTraceElement elem) { - var maxLineNumWidth = String.valueOf(lines.lastKey()).length(); - return lines.entrySet().stream() - .map( - entry -> { - var lineNum = entry.getKey(); - var isTarget = lineNum == elem.getLineNumber(); - var line = entry.getValue(); - var lineNumStr = format("%" + maxLineNumWidth + "d", lineNum); - return format( - "%s %s%s", isTarget ? "->" : " ", lineNumStr, line.isEmpty() ? "" : " " + line); - }) - .collect(toList()); - } - - private static String getStackTraceAsString(Throwable e) { - var stringWriter = new StringWriter(); - var printWriter = new PrintWriter(stringWriter); - e.printStackTrace(printWriter); - printWriter.flush(); - return stringWriter.toString(); - } - - private static void pruneStackTrace( - Throwable throwable, Class<?> keepFromClass, Set<Throwable> alreadySeen) { - if (!alreadySeen.add(throwable)) { - return; - } - - var stackTrace = throwable.getStackTrace(); - for (var i = stackTrace.length - 1; i >= 0; i--) { - if (stackTrace[i].getClassName().equals(keepFromClass.getName())) { - throwable.setStackTrace(Arrays.copyOfRange(stackTrace, 0, i + 1)); - break; - } - } - - var cause = throwable.getCause(); - if (cause != null) { - pruneStackTrace(cause, keepFromClass, alreadySeen); - } - - for (var suppressed : throwable.getSuppressed()) { - pruneStackTrace(suppressed, keepFromClass, alreadySeen); - } - } -} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 156aabc83880f8..a3d8ebd792d96b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -65,6 +65,7 @@ import io.quarkus.runtime.configuration.ConfigInstantiator; import io.quarkus.runtime.configuration.ConfigUtils; import io.quarkus.runtime.configuration.MemorySize; +import io.quarkus.runtime.logging.LogBuildTimeConfig; import io.quarkus.runtime.shutdown.ShutdownConfig; import io.quarkus.tls.TlsConfigurationRegistry; import io.quarkus.tls.runtime.config.TlsConfig; @@ -379,6 +380,9 @@ public void finalizeRouter(BeanContainer container, Consumer<Route> defaultRoute Handler<RoutingContext> bodyHandler, GracefulShutdownFilter gracefulShutdownFilter, ShutdownConfig shutdownConfig, Executor executor, + LogBuildTimeConfig logBuildTimeConfig, + String srcMainJava, + List<String> knowClasses, List<ErrorPageAction> actions) { HttpConfiguration httpConfiguration = this.httpConfiguration.getValue(); // install the default route at the end @@ -416,8 +420,8 @@ public void finalizeRouter(BeanContainer container, Consumer<Route> defaultRoute applyCompression(httpBuildTimeConfig.enableCompression, httpRouteRouter); httpRouteRouter.route().last().failureHandler( - new QuarkusErrorHandler(launchMode.isDevOrTest(), decorateStacktrace(launchMode, httpConfiguration), - httpConfiguration.unhandledErrorContentTypeDefault, actions)); + new QuarkusErrorHandler(launchMode.isDevOrTest(), decorateStacktrace(launchMode, logBuildTimeConfig), + httpConfiguration.unhandledErrorContentTypeDefault, srcMainJava, knowClasses, actions)); for (BooleanSupplier requireBodyHandlerCondition : requireBodyHandlerConditions) { if (requireBodyHandlerCondition.getAsBoolean()) { //if this is set then everything needs the body handler installed @@ -535,8 +539,8 @@ public void handle(RoutingContext event) { addHotReplacementHandlerIfNeeded(mr); mr.route().last().failureHandler( - new QuarkusErrorHandler(launchMode.isDevOrTest(), decorateStacktrace(launchMode, httpConfiguration), - httpConfiguration.unhandledErrorContentTypeDefault, actions)); + new QuarkusErrorHandler(launchMode.isDevOrTest(), decorateStacktrace(launchMode, logBuildTimeConfig), + httpConfiguration.unhandledErrorContentTypeDefault, srcMainJava, knowClasses, actions)); mr.route().order(RouteConstants.ROUTE_ORDER_BODY_HANDLER_MANAGEMENT) .handler(createBodyHandlerForManagementInterface()); @@ -577,8 +581,8 @@ public void handle(HttpServerRequest event) { } } - private boolean decorateStacktrace(LaunchMode launchMode, HttpConfiguration httpConfiguration) { - return httpConfiguration.decorateStacktraces && launchMode.equals(LaunchMode.DEVELOPMENT); + private boolean decorateStacktrace(LaunchMode launchMode, LogBuildTimeConfig logBuildTimeConfig) { + return logBuildTimeConfig.decorateStacktraces && launchMode.equals(LaunchMode.DEVELOPMENT); } private void addHotReplacementHandlerIfNeeded(Router router) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ide/IdeRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ide/IdeRecorder.java new file mode 100644 index 00000000000000..ee3ed928363431 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ide/IdeRecorder.java @@ -0,0 +1,23 @@ +package io.quarkus.vertx.http.runtime.ide; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.runtime.annotations.Recorder; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +@Recorder +public class IdeRecorder { + + public Handler<RoutingContext> openInIde() { + return new Handler<RoutingContext>() { + @Override + public void handle(RoutingContext rc) { + DevConsoleManager.invoke("devui-ide-interaction.open", rc.pathParams()); + rc.response() + .setStatusCode(HttpResponseStatus.ACCEPTED.code()).end(); + } + }; + } + +} diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 8c5d506d6e1a64..f8e32ba6b1c88c 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -45,7 +45,7 @@ <version.jta>2.0.1</version.jta> <!-- main versions --> <version.gizmo>1.8.0</version.gizmo> - <version.jandex>3.2.0</version.jandex> + <version.jandex>3.2.1</version.jandex> <version.jboss-logging>3.6.0.Final</version.jboss-logging> <version.mutiny>2.6.2</version.mutiny> <version.bridger>1.6.Final</version.bridger> @@ -238,36 +238,6 @@ <excludedEnvironmentVariables>MAVEN_OPTS</excludedEnvironmentVariables> </configuration> </plugin> - <plugin> - <groupId>net.revelc.code.formatter</groupId> - <artifactId>formatter-maven-plugin</artifactId> - <dependencies> - <dependency> - <artifactId>quarkus-ide-config</artifactId> - <groupId>io.quarkus</groupId> - <version>${project.version}</version> - </dependency> - </dependencies> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/formatter-maven-plugin-${formatter-maven-plugin.version}</cachedir> - <configFile>eclipse-format.xml</configFile> - <lineEnding>LF</lineEnding> - <skip>${format.skip}</skip> - </configuration> - </plugin> - <plugin> - <groupId>net.revelc.code</groupId> - <artifactId>impsort-maven-plugin</artifactId> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/impsort-maven-plugin-${impsort-maven-plugin.version}</cachedir> - <groups>java.,javax.,jakarta.,org.,com.</groups> - <staticGroups>*</staticGroups> - <skip>${format.skip}</skip> - <removeUnused>true</removeUnused> - </configuration> - </plugin> </plugins> </pluginManagement> </build> diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index 89e16cf4f024c5..2e0191ac1b15c3 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -34,7 +34,7 @@ <javax.annotation-api.version>1.3.2</javax.annotation-api.version> <javax.inject.version>1</javax.inject.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - <jandex.version>3.2.0</jandex.version> + <jandex.version>3.2.1</jandex.version> <jmh.version>1.37</jmh.version> <!-- Dependency versions --> @@ -76,8 +76,6 @@ <gradle-tooling.version>8.9</gradle-tooling.version> <quarkus-fs-util.version>0.0.10</quarkus-fs-util.version> <org-crac.version>0.1.3</org-crac.version> - <formatter-maven-plugin.version>2.24.1</formatter-maven-plugin.version> - <impsort-maven-plugin.version>1.10.0</impsort-maven-plugin.version> </properties> <modules> <module>bom</module> @@ -160,37 +158,6 @@ <excludedEnvironmentVariables>MAVEN_OPTS</excludedEnvironmentVariables> </configuration> </plugin> - <!-- Replicate what's in parent, since this pom doesn't inherit parent but IDE settings will be common --> - <plugin> - <groupId>net.revelc.code.formatter</groupId> - <artifactId>formatter-maven-plugin</artifactId> - <dependencies> - <dependency> - <artifactId>quarkus-ide-config</artifactId> - <groupId>io.quarkus</groupId> - <version>${project.version}</version> - </dependency> - </dependencies> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/formatter-maven-plugin-${formatter-maven-plugin.version}</cachedir> - <configFile>eclipse-format.xml</configFile> - <lineEnding>LF</lineEnding> - <skip>${format.skip}</skip> - </configuration> - </plugin> - <plugin> - <groupId>net.revelc.code</groupId> - <artifactId>impsort-maven-plugin</artifactId> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/impsort-maven-plugin-${impsort-maven-plugin.version}</cachedir> - <groups>java.,javax.,jakarta.,org.,com.</groups> - <staticGroups>*</staticGroups> - <skip>${format.skip}</skip> - <removeUnused>true</removeUnused> - </configuration> - </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-plugin-plugin</artifactId> 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 new file mode 100644 index 00000000000000..e02c92d3ba8691 --- /dev/null +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarFileReference.java @@ -0,0 +1,176 @@ +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) { + jarResource.jarFileReference.set(null); + try { + jarFile.close(); + } catch (IOException e) { + // ignore + } + 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> { + T apply(JarFile jarFile, Path jarPath, String resource); + } + + static <T> T withJarFile(JarResource jarResource, String resource, JarFileConsumer<T> 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), 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> T consumeSharedJarFile(JarFileReference jarFileReference, + JarResource jarResource, String resource, JarFileConsumer<T> fileConsumer) { + try { + return fileConsumer.apply(jarFileReference.jarFile, jarResource.jarPath, resource); + } finally { + jarFileReference.release(jarResource); + } + } + + private static <T> T consumeUnsharedJarFile(CompletableFuture<JarFileReference> jarFileReferenceFuture, + JarResource jarResource, String resource, JarFileConsumer<T> 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<JarFileReference> 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 JarFileReference asyncLoadAcquiredJarFile(JarResource jarResource) { + CompletableFuture<JarFileReference> newJarRefFuture = new CompletableFuture<>(); + CompletableFuture<JarFileReference> existingJarRefFuture = null; + JarFileReference existingJarRef = null; + + do { + if (jarResource.jarFileReference.compareAndSet(null, newJarRefFuture)) { + try { + JarFileReference newJarRef = new JarFileReference(JarFiles.create(jarResource.jarPath.toFile())); + newJarRefFuture.complete(newJarRef); + return newJarRef; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + existingJarRefFuture = jarResource.jarFileReference.get(); + existingJarRef = existingJarRefFuture == null ? null : existingJarRefFuture.join(); + } while (existingJarRef == null || !existingJarRef.acquire()); + return existingJarRef; + } +} 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 8b4da096a891d1..84ae3b69246a28 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,44 +11,28 @@ import java.security.ProtectionDomain; import java.security.cert.Certificate; import java.util.Objects; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; 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 final ManifestInfo manifestInfo; - private final Path jarPath; - - private final Lock readLock; - private final Lock writeLock; - private volatile ProtectionDomain protectionDomain; + private final ManifestInfo manifestInfo; - //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; + final Path jarPath; + final AtomicReference<CompletableFuture<JarFileReference>> jarFileReference = new AtomicReference<>(); 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 @@ -69,38 +53,48 @@ public void init() { @Override public byte[] getResourceData(String resource) { - final ZipFile zipFile = readLockAcquireAndGetJarReference(); - try { - ZipEntry entry = zipFile.getEntry(resource); + return JarFileReference.withJarFile(this, resource, JarResourceDataProvider.INSTANCE); + } + + private static class JarResourceDataProvider implements JarFileReference.JarFileConsumer<byte[]> { + private static final JarResourceDataProvider INSTANCE = new JarResourceDataProvider(); + + @Override + public byte[] apply(JarFile jarFile, Path path, String res) { + ZipEntry entry = jarFile.getEntry(res); if (entry == null) { return null; } - try (InputStream is = zipFile.getInputStream(entry)) { + try (InputStream is = jarFile.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 " + resource); + throw new RuntimeException("Failed to read all data for " + res); } pos += read; rem -= read; } return data; } catch (IOException e) { - throw new RuntimeException("Failed to read zip entry " + resource, e); + throw new RuntimeException("Failed to read zip entry " + res, e); } - } finally { - readLock.unlock(); } } @Override public URL getResourceURL(String resource) { - final JarFile jarFile = readLockAcquireAndGetJarReference(); - try { - JarEntry entry = jarFile.getJarEntry(resource); + return JarFileReference.withJarFile(this, resource, JarResourceURLProvider.INSTANCE); + } + + private static class JarResourceURLProvider implements JarFileReference.JarFileConsumer<URL> { + private static final JarResourceURLProvider INSTANCE = new JarResourceURLProvider(); + + @Override + public URL apply(JarFile jarFile, Path path, String res) { + JarEntry entry = jarFile.getJarEntry(res); if (entry == null) { return null; } @@ -110,15 +104,7 @@ public URL getResourceURL(String resource) { if (realName.endsWith("/")) { realName = realName.substring(0, realName.length() - 1); } - 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(); + final URL resUrl = getUrl(path, realName); // 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 @@ -136,8 +122,18 @@ public URL getResourceURL(String resource) { } catch (MalformedURLException | URISyntaxException e) { throw new RuntimeException(e); } - } finally { - readLock.unlock(); + } + + 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(); } } @@ -151,60 +147,16 @@ 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() { - writeLock.lock(); - try { - final JarFile zipFileLocal = this.zipFile; - if (zipFileLocal != null) { - try { - this.zipFile = null; - zipFileLocal.close(); - } catch (Throwable e) { - //ignore - } + 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); } - } 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 071d77ffef15f2..42599e6ab11315 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 @@ -29,6 +29,10 @@ */ 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 */ @@ -104,18 +108,55 @@ public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundExce continue; } definePackage(packageName, resources); - try { - return defineClass(name, data, 0, data.length, resource.getProtectionDomain()); - } catch (LinkageError e) { - loaded = findLoadedClass(name); - if (loaded != null) { - return loaded; + 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; + } } + return; + } + } + try { + definePackage(pkgName, null, null, null, null, null, null, null); + } catch (IllegalArgumentException e) { + var loaded = getDefinedPackage(pkgName); + if (loaded == null) { throw e; } } } - return getParent().loadClass(name); + } + + 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; + } } private void accessingResource(final ClassLoadingResource resource) { @@ -132,25 +173,33 @@ 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 - final ClassLoadingResource previous = currentlyBufferedResources[i - 1]; - currentlyBufferedResources[i - 1] = resource; - currentlyBufferedResources[i] = previous; + bubbleUpCachedResource(resource, i); return; } } + // else, we drop one element from the cache, // and inserting the latest resource on the tail: toEvict = currentlyBufferedResources[currentlyBufferedResources.length - 1]; - currentlyBufferedResources[currentlyBufferedResources.length - 1] = resource; + bubbleUpCachedResource(resource, currentlyBufferedResources.length - 1); } + // 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); @@ -222,28 +271,6 @@ protected Enumeration<URL> 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 new file mode 100644 index 00000000000000..5d6f03a51a3ab2 --- /dev/null +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/VirtualThreadSupport.java @@ -0,0 +1,52 @@ +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/bootstrap/runner/src/test/java/io/quarkus/bootstrap/runner/RunnerClassLoaderTest.java b/independent-projects/bootstrap/runner/src/test/java/io/quarkus/bootstrap/runner/RunnerClassLoaderTest.java new file mode 100644 index 00000000000000..8e4c902ad73d95 --- /dev/null +++ b/independent-projects/bootstrap/runner/src/test/java/io/quarkus/bootstrap/runner/RunnerClassLoaderTest.java @@ -0,0 +1,79 @@ +package io.quarkus.bootstrap.runner; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +public class RunnerClassLoaderTest { + + @Test + public void testConcurrentJarCloseAndReload() throws Exception { + Map<String, ClassLoadingResource[]> resourceDirectoryMap = new HashMap<>(); + + resourceDirectoryMap.put("org/simple", new ClassLoadingResource[] { + createProjectJarResource("simple-project-1.0.jar") }); + + // These jars are simply used to fill the RunnerClassLoader's jars cache + resourceDirectoryMap.put("org/easy", new ClassLoadingResource[] { + createProjectJarResource("empty-project-a-1.0.jar"), createProjectJarResource("empty-project-b-1.0.jar"), + createProjectJarResource("easy-project-1.0.jar") }); + + resourceDirectoryMap.put("org/trivial", new ClassLoadingResource[] { + createProjectJarResource("trivial-project-1.0.jar") }); + + RunnerClassLoader runnerClassLoader = new RunnerClassLoader(ClassLoader.getSystemClassLoader(), resourceDirectoryMap, + Collections.emptySet(), Collections.emptySet(), + Collections.emptyList(), Collections.emptyMap()); + + // Put the RunnerClassLoader in a postBootPhase thus enabling the jars cache + runnerClassLoader.resetInternalCaches(); + + runnerClassLoader.loadClass("org.simple.SimplePojo1"); + + // Fills the RunnerClassLoader's jars cache + runnerClassLoader.loadClass("org.easy.EasyPojo"); + + // Now easy-project-1.0.jar is the least recently used jar in cache and the next to be evicted when a class + // from a different jar will be requested + + ExecutorService executor = Executors.newFixedThreadPool(2); + + Runnable evictingTask = () -> { + try { + runnerClassLoader.loadClass("org.trivial.TrivialPojo"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }; + Runnable reloadingTask = () -> { + try { + runnerClassLoader.loadClass("org.simple.SimplePojo2"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }; + + // Concurrently executes the task evicting the jar and the one reopening it + try { + executor.submit(evictingTask); + executor.submit(reloadingTask); + } finally { + executor.shutdown(); + executor.awaitTermination(1, TimeUnit.SECONDS); + } + } + + private static JarResource createProjectJarResource(String jarName) { + ManifestInfo manifestInfo = new ManifestInfo(jarName.substring(jarName.lastIndexOf('-')), "1.0", "Apache", null, null, + null); + JarResource jarResource = new JarResource(manifestInfo, + Path.of("src", "test", "resources", "jars", jarName)); + return jarResource; + } +} diff --git a/independent-projects/bootstrap/runner/src/test/resources/jars/easy-project-1.0.jar b/independent-projects/bootstrap/runner/src/test/resources/jars/easy-project-1.0.jar new file mode 100644 index 00000000000000..8ab794415a4504 Binary files /dev/null and b/independent-projects/bootstrap/runner/src/test/resources/jars/easy-project-1.0.jar differ diff --git a/independent-projects/bootstrap/runner/src/test/resources/jars/empty-project-a-1.0.jar b/independent-projects/bootstrap/runner/src/test/resources/jars/empty-project-a-1.0.jar new file mode 100644 index 00000000000000..ace16ce6b2a77f Binary files /dev/null and b/independent-projects/bootstrap/runner/src/test/resources/jars/empty-project-a-1.0.jar differ diff --git a/independent-projects/bootstrap/runner/src/test/resources/jars/empty-project-b-1.0.jar b/independent-projects/bootstrap/runner/src/test/resources/jars/empty-project-b-1.0.jar new file mode 100644 index 00000000000000..ace16ce6b2a77f Binary files /dev/null and b/independent-projects/bootstrap/runner/src/test/resources/jars/empty-project-b-1.0.jar differ diff --git a/independent-projects/bootstrap/runner/src/test/resources/jars/simple-project-1.0.jar b/independent-projects/bootstrap/runner/src/test/resources/jars/simple-project-1.0.jar new file mode 100644 index 00000000000000..6740ac12200444 Binary files /dev/null and b/independent-projects/bootstrap/runner/src/test/resources/jars/simple-project-1.0.jar differ diff --git a/independent-projects/bootstrap/runner/src/test/resources/jars/trivial-project-1.0.jar b/independent-projects/bootstrap/runner/src/test/resources/jars/trivial-project-1.0.jar new file mode 100644 index 00000000000000..0752cca9e26b59 Binary files /dev/null and b/independent-projects/bootstrap/runner/src/test/resources/jars/trivial-project-1.0.jar differ diff --git a/independent-projects/enforcer-rules/pom.xml b/independent-projects/enforcer-rules/pom.xml index c035de0408158a..d0caa41c5de8c3 100644 --- a/independent-projects/enforcer-rules/pom.xml +++ b/independent-projects/enforcer-rules/pom.xml @@ -108,36 +108,6 @@ </dependency> </dependencies> </plugin> - <plugin> - <groupId>net.revelc.code.formatter</groupId> - <artifactId>formatter-maven-plugin</artifactId> - <dependencies> - <dependency> - <artifactId>quarkus-ide-config</artifactId> - <groupId>io.quarkus</groupId> - <version>${project.version}</version> - </dependency> - </dependencies> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/formatter-maven-plugin-${formatter-maven-plugin.version}</cachedir> - <configFile>eclipse-format.xml</configFile> - <lineEnding>LF</lineEnding> - <skip>${format.skip}</skip> - </configuration> - </plugin> - <plugin> - <groupId>net.revelc.code</groupId> - <artifactId>impsort-maven-plugin</artifactId> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/impsort-maven-plugin-${impsort-maven-plugin.version}</cachedir> - <groups>java.,javax.,jakarta.,org.,com.</groups> - <staticGroups>*</staticGroups> - <skip>${format.skip}</skip> - <removeUnused>true</removeUnused> - </configuration> - </plugin> </plugins> </build> diff --git a/independent-projects/extension-maven-plugin/pom.xml b/independent-projects/extension-maven-plugin/pom.xml index 970ee267258134..867fccc73cb9cf 100644 --- a/independent-projects/extension-maven-plugin/pom.xml +++ b/independent-projects/extension-maven-plugin/pom.xml @@ -127,36 +127,6 @@ <excludedEnvironmentVariables>MAVEN_OPTS</excludedEnvironmentVariables> </configuration> </plugin> - <plugin> - <groupId>net.revelc.code.formatter</groupId> - <artifactId>formatter-maven-plugin</artifactId> - <dependencies> - <dependency> - <artifactId>quarkus-ide-config</artifactId> - <groupId>io.quarkus</groupId> - <version>${project.version}</version> - </dependency> - </dependencies> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/formatter-maven-plugin-${formatter-maven-plugin.version}</cachedir> - <configFile>eclipse-format.xml</configFile> - <lineEnding>LF</lineEnding> - <skip>${format.skip}</skip> - </configuration> - </plugin> - <plugin> - <groupId>net.revelc.code</groupId> - <artifactId>impsort-maven-plugin</artifactId> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/impsort-maven-plugin-${impsort-maven-plugin.version}</cachedir> - <groups>java.,javax.,jakarta.,org.,com.</groups> - <staticGroups>*</staticGroups> - <skip>${format.skip}</skip> - <removeUnused>true</removeUnused> - </configuration> - </plugin> </plugins> </pluginManagement> <plugins> diff --git a/independent-projects/junit5-virtual-threads/pom.xml b/independent-projects/junit5-virtual-threads/pom.xml index c999a9bebfb93e..bca5d520aae21d 100644 --- a/independent-projects/junit5-virtual-threads/pom.xml +++ b/independent-projects/junit5-virtual-threads/pom.xml @@ -41,9 +41,7 @@ <compiler.plugin.version>3.13.0</compiler.plugin.version> <enforcer.plugin.version>3.2.1</enforcer.plugin.version> <surefire.plugin.version>3.2.5</surefire.plugin.version> - <jandex.version>3.2.0</jandex.version> - <formatter-maven-plugin.version>2.24.1</formatter-maven-plugin.version> - <impsort-maven-plugin.version>1.11.0</impsort-maven-plugin.version> + <jandex.version>3.2.1</jandex.version> <junit.jupiter.version>5.10.3</junit.jupiter.version> <junit.testkit.version>1.10.3</junit.testkit.version> @@ -146,39 +144,6 @@ </excludes> </configuration> </plugin> - <!-- Replicate what's in parent, since this pom doesn't inherit parent but IDE settings will be common --> - <plugin> - <groupId>net.revelc.code.formatter</groupId> - <artifactId>formatter-maven-plugin</artifactId> - <version>${formatter-maven-plugin.version}</version> - <dependencies> - <dependency> - <artifactId>quarkus-ide-config</artifactId> - <groupId>io.quarkus</groupId> - <version>${project.version}</version> - </dependency> - </dependencies> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/formatter-maven-plugin-${formatter-maven-plugin.version}</cachedir> - <configFile>eclipse-format.xml</configFile> - <lineEnding>LF</lineEnding> - <skip>${format.skip}</skip> - </configuration> - </plugin> - <plugin> - <groupId>net.revelc.code</groupId> - <artifactId>impsort-maven-plugin</artifactId> - <version>${impsort-maven-plugin.version}</version> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/impsort-maven-plugin-${impsort-maven-plugin.version}</cachedir> - <groups>java.,javax.,jakarta.,org.,com.</groups> - <staticGroups>*</staticGroups> - <skip>${format.skip}</skip> - <removeUnused>true</removeUnused> - </configuration> - </plugin> </plugins> </pluginManagement> </build> diff --git a/independent-projects/parent/pom.xml b/independent-projects/parent/pom.xml index 6fe366aad2ab80..5d2c28da28836a 100644 --- a/independent-projects/parent/pom.xml +++ b/independent-projects/parent/pom.xml @@ -353,7 +353,17 @@ <groupId>net.revelc.code.formatter</groupId> <artifactId>formatter-maven-plugin</artifactId> <version>${version.formatter.plugin}</version> + <dependencies> + <dependency> + <artifactId>quarkus-ide-config</artifactId> + <groupId>io.quarkus</groupId> + <version>${project.version}</version> + </dependency> + </dependencies> <configuration> + <!-- store outside of target to speed up formatting when mvn clean is used --> + <cachedir>.cache/formatter-maven-plugin-${version.formatter.plugin}</cachedir> + <configFile>eclipse-format.xml</configFile> <lineEnding>LF</lineEnding> <skip>${format.skip}</skip> </configuration> @@ -363,6 +373,8 @@ <artifactId>impsort-maven-plugin</artifactId> <version>${version.impsort.plugin}</version> <configuration> + <!-- store outside of target to speed up formatting when mvn clean is used --> + <cachedir>.cache/impsort-maven-plugin-${version.impsort.plugin}</cachedir> <groups>java.,javax.,jakarta.,org.,com.</groups> <staticGroups>*</staticGroups> <skip>${format.skip}</skip> diff --git a/independent-projects/qute/pom.xml b/independent-projects/qute/pom.xml index 9e1353a1376091..11594905f2d329 100644 --- a/independent-projects/qute/pom.xml +++ b/independent-projects/qute/pom.xml @@ -40,7 +40,7 @@ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <version.junit>5.10.3</version.junit> <version.assertj>3.26.3</version.assertj> - <version.jandex>3.2.0</version.jandex> + <version.jandex>3.2.1</version.jandex> <version.gizmo>1.8.0</version.gizmo> <version.jboss-logging>3.6.0.Final</version.jboss-logging> <version.smallrye-mutiny>2.6.2</version.smallrye-mutiny> @@ -156,36 +156,6 @@ <excludedEnvironmentVariables>MAVEN_OPTS</excludedEnvironmentVariables> </configuration> </plugin> - <plugin> - <groupId>net.revelc.code.formatter</groupId> - <artifactId>formatter-maven-plugin</artifactId> - <dependencies> - <dependency> - <artifactId>quarkus-ide-config</artifactId> - <groupId>io.quarkus</groupId> - <version>${project.version}</version> - </dependency> - </dependencies> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/formatter-maven-plugin-${formatter-maven-plugin.version}</cachedir> - <configFile>eclipse-format.xml</configFile> - <lineEnding>LF</lineEnding> - <skip>${format.skip}</skip> - </configuration> - </plugin> - <plugin> - <groupId>net.revelc.code</groupId> - <artifactId>impsort-maven-plugin</artifactId> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/impsort-maven-plugin-${impsort-maven-plugin.version}</cachedir> - <groups>java.,javax.,jakarta.,org.,com.</groups> - <staticGroups>*</staticGroups> - <skip>${format.skip}</skip> - <removeUnused>true</removeUnused> - </configuration> - </plugin> </plugins> </pluginManagement> </build> diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 5d8b59b862610d..d7e71c9fe66618 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -41,11 +41,11 @@ <jakarta.json-api.version>2.1.3</jakarta.json-api.version> <jakarta.ws.rs-api.version>3.1.0</jakarta.ws.rs-api.version> <jakarta.xml.bind-api.version>4.0.2</jakarta.xml.bind-api.version> - <parsson.version>1.1.6</parsson.version> + <parsson.version>1.1.7</parsson.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- Versions --> <jakarta.enterprise.cdi-api.version>4.1.0</jakarta.enterprise.cdi-api.version> - <jandex.version>3.2.0</jandex.version> + <jandex.version>3.2.1</jandex.version> <bytebuddy.version>1.14.11</bytebuddy.version> <junit5.version>5.10.3</junit5.version> <maven.version>3.9.8</maven.version> @@ -457,36 +457,6 @@ <excludedEnvironmentVariables>MAVEN_OPTS</excludedEnvironmentVariables> </configuration> </plugin> - <plugin> - <groupId>net.revelc.code.formatter</groupId> - <artifactId>formatter-maven-plugin</artifactId> - <dependencies> - <dependency> - <artifactId>quarkus-ide-config</artifactId> - <groupId>io.quarkus</groupId> - <version>${project.version}</version> - </dependency> - </dependencies> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/formatter-maven-plugin-${formatter-maven-plugin.version}</cachedir> - <configFile>eclipse-format.xml</configFile> - <lineEnding>LF</lineEnding> - <skip>${format.skip}</skip> - </configuration> - </plugin> - <plugin> - <groupId>net.revelc.code</groupId> - <artifactId>impsort-maven-plugin</artifactId> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/impsort-maven-plugin-${impsort-maven-plugin.version}</cachedir> - <groups>java.,javax.,jakarta.,org.,com.</groups> - <staticGroups>*</staticGroups> - <skip>${format.skip}</skip> - <removeUnused>true</removeUnused> - </configuration> - </plugin> <plugin> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy-maven-plugin</artifactId> diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerExceptionMapper.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerExceptionMapper.java index 7a4689f9ca757c..2d0df9e1e8f145 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerExceptionMapper.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerExceptionMapper.java @@ -41,6 +41,8 @@ * <p> * The return type of the method must be either be of type {@code Response}, {@code Uni<Response>}, {@code RestResponse} or * {@code Uni<RestResponse>}. + * <p> + * See also {@link UnwrapException} */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/UnwrapException.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/UnwrapException.java new file mode 100644 index 00000000000000..430757f67ebaac --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/UnwrapException.java @@ -0,0 +1,23 @@ +package org.jboss.resteasy.reactive.server; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to configure that an exception (or exceptions) should be unwrapped during exception handling. + * <p> + * Unwrapping means that when an {@link Exception} of the configured type is thrown and no + * {@code jakarta.ws.rs.ext.ExceptionMapper} exists, + * then RESTEasy Reactive will attempt to locate an {@code ExceptionMapper} for the cause of the Exception. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface UnwrapException { + + /** + * If this is not set, the value is assumed to be the exception class where the annotation is placed + */ + Class<? extends Exception>[] value() default {}; +} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute index 286a7757dbc7c7..26f92d39702789 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/base/Dockerfile-layout.include.qute @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-{java.version}:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-{java.version}:1.20 ENV LANGUAGE='en_US:en' diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml index 22e7403ac8e26e..df807e43d53c9c 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/dockerfiles/codestart.yml @@ -5,6 +5,6 @@ language: data: dockerfile: native: - from: registry.access.redhat.com/ubi8/ubi-minimal:8.9 + from: registry.access.redhat.com/ubi8/ubi-minimal:8.10 native-micro: from: quay.io/quarkus/quarkus-micro-image:2.0 diff --git a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java index 36dd1124359ad7..5464bb7dfa7a4d 100644 --- a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java +++ b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartGenerationTest.java @@ -302,13 +302,13 @@ private void checkDockerfilesWithMaven(Path projectDir) { assertThat(projectDir.resolve("src/main/docker/Dockerfile.jvm")).exists() .satisfies(checkContains("./mvnw package")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.jvm")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.19")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.20")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) .satisfies(checkContains("ENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]")); assertThat(projectDir.resolve("src/main/docker/Dockerfile.legacy-jar")).exists() .satisfies(checkContains("./mvnw package -Dquarkus.package.jar.type=legacy-jar")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.legacy-jar")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.19")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.20")) .satisfies(checkContains("EXPOSE 8080")) .satisfies(checkContains("USER 185")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) @@ -328,13 +328,13 @@ private void checkDockerfilesWithGradle(Path projectDir) { assertThat(projectDir.resolve("src/main/docker/Dockerfile.jvm")).exists() .satisfies(checkContains("./gradlew build")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.jvm")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.19")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.20")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) .satisfies(checkContains("ENTRYPOINT [ \"/opt/jboss/container/java/run/run-java.sh\" ]")); assertThat(projectDir.resolve("src/main/docker/Dockerfile.legacy-jar")).exists() .satisfies(checkContains("./gradlew build -Dquarkus.package.jar.type=legacy-jar")) .satisfies(checkContains("docker build -f src/main/docker/Dockerfile.legacy-jar")) - .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.19")) + .satisfies(checkContains("registry.access.redhat.com/ubi8/openjdk-17:1.20")) .satisfies(checkContains("EXPOSE 8080")) .satisfies(checkContains("USER 185")) .satisfies(checkContains("ENV JAVA_APP_JAR=\"/deployments/quarkus-run.jar\"")) diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index e507dcd8e98e45..ab2a0c80fe6a5b 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -57,7 +57,7 @@ <mockito.version>5.12.0</mockito.version> <quarkus.version>${project.version}</quarkus.version> <maven-model-helper.version>37</maven-model-helper.version> - <jandex.version>3.2.0</jandex.version> + <jandex.version>3.2.1</jandex.version> <system-stubs-jupiter.version>2.0.2</system-stubs-jupiter.version> <awaitility.version>4.2.1</awaitility.version> <java-properties.version>0.0.7</java-properties.version> @@ -253,36 +253,6 @@ <excludedEnvironmentVariables>MAVEN_OPTS</excludedEnvironmentVariables> </configuration> </plugin> - <plugin> - <groupId>net.revelc.code.formatter</groupId> - <artifactId>formatter-maven-plugin</artifactId> - <dependencies> - <dependency> - <artifactId>quarkus-ide-config</artifactId> - <groupId>io.quarkus</groupId> - <version>${project.version}</version> - </dependency> - </dependencies> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/formatter-maven-plugin-${formatter-maven-plugin.version}</cachedir> - <configFile>eclipse-format.xml</configFile> - <lineEnding>LF</lineEnding> - <skip>${format.skip}</skip> - </configuration> - </plugin> - <plugin> - <groupId>net.revelc.code</groupId> - <artifactId>impsort-maven-plugin</artifactId> - <configuration> - <!-- store outside of target to speed up formatting when mvn clean is used --> - <cachedir>.cache/impsort-maven-plugin-${impsort-maven-plugin.version}</cachedir> - <groups>java.,javax.,jakarta.,org.,com.</groups> - <staticGroups>*</staticGroups> - <skip>${format.skip}</skip> - <removeUnused>true</removeUnused> - </configuration> - </plugin> </plugins> </build> diff --git a/integration-tests/awt/src/main/docker/Dockerfile.native b/integration-tests/awt/src/main/docker/Dockerfile.native index e2ff5cf61ed948..c0db67b9018a69 100644 --- a/integration-tests/awt/src/main/docker/Dockerfile.native +++ b/integration-tests/awt/src/main/docker/Dockerfile.native @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 # Dependencies for AWT RUN microdnf install freetype fontconfig \ && microdnf clean all diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm index 423791b5a44b95..6b79015745dc2e 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-docker/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm index 6de170527f1ed6..21f6444cc94bc4 100644 --- a/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm +++ b/integration-tests/container-image/maven-invoker-way/src/it/container-build-multiple-tags-docker/src/main/docker/Dockerfile.jvm @@ -21,7 +21,7 @@ # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/rest-client-quickstart-jvm # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 ARG JAVA_PACKAGE=java-17-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm index 423791b5a44b95..6b79015745dc2e 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-deployment/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm index 423791b5a44b95..6b79015745dc2e 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-docker-build-and-deploy-statefulset/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm index 423791b5a44b95..6b79015745dc2e 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-jib-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm index 423791b5a44b95..6b79015745dc2e 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/kubernetes-with-existing-selectorless-manifest/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/src/main/docker/Dockerfile.jvm index 423791b5a44b95..6b79015745dc2e 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/minikube-with-existing-manifest/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy-deploymentconfig/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy-deploymentconfig/src/main/docker/Dockerfile.jvm index 423791b5a44b95..6b79015745dc2e 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy-deploymentconfig/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy-deploymentconfig/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm index 423791b5a44b95..6b79015745dc2e 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-docker-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm index 423791b5a44b95..6b79015745dc2e 100644 --- a/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm +++ b/integration-tests/kubernetes/maven-invoker-way/src/it/openshift-s2i-build-and-deploy/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm index c3dd7f16cdc5db..3f22b0ccac266f 100644 --- a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm +++ b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.jvm @@ -75,7 +75,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar index 9205b7cbf71f71..d20fbfd7b9ab10 100644 --- a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar +++ b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.legacy-jar @@ -75,7 +75,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.19 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 ENV LANGUAGE='en_US:en' diff --git a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.native b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.native index 3e3e2fa098b965..af439d708a8d29 100644 --- a/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.native +++ b/integration-tests/maven/src/test/resources-filtered/projects/codegen-config-factory/app/src/main/docker/Dockerfile.native @@ -14,7 +14,7 @@ # docker run -i --rm -p 8080:8080 quarkus/code-with-quarkus # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 WORKDIR /work/ RUN chown 1001 /work \ && chmod "g+rwX" /work \ diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPConfigSourceInterceptor.java b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPConfigSourceInterceptor.java new file mode 100644 index 00000000000000..a5c059a2b39cb5 --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPConfigSourceInterceptor.java @@ -0,0 +1,49 @@ +package io.quarkus.test.common.http; + +import static io.quarkus.test.common.http.TestHTTPConfigSourceProvider.TEST_MANAGEMENT_URL_KEY; +import static io.quarkus.test.common.http.TestHTTPConfigSourceProvider.TEST_MANAGEMENT_URL_SSL_KEY; +import static io.quarkus.test.common.http.TestHTTPConfigSourceProvider.TEST_URL_KEY; +import static io.quarkus.test.common.http.TestHTTPConfigSourceProvider.TEST_URL_SSL_KEY; + +import jakarta.annotation.Priority; + +import io.smallrye.config.ConfigSourceInterceptorContext; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.ExpressionConfigSourceInterceptor; +import io.smallrye.config.Priorities; + +/** + * Override the expression expansion for test urls so they can be sanitized. A simple interceptor does not work + * because the test urls values are nested expressions, so when the default expression interceptor runs, either we get + * the full value expanded or the value unexpanded. In most cases, the test urls are used as expressions, so we need to + * intercept the expression expansion directly to rewrite what we need. + */ +@Priority(Priorities.LIBRARY + 299) +public class TestHTTPConfigSourceInterceptor extends ExpressionConfigSourceInterceptor { + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + if (name.equals(TEST_URL_KEY) || + name.equals(TEST_MANAGEMENT_URL_KEY) || + name.equals(TEST_URL_SSL_KEY) || + name.equals(TEST_MANAGEMENT_URL_SSL_KEY)) { + + return sanitizeUrl(super.getValue(context, name)); + } + + return context.proceed(name); + } + + private static ConfigValue sanitizeUrl(ConfigValue configValue) { + if (configValue == null || configValue.getValue() == null) { + return configValue; + } + + String url = configValue.getValue(); + url = url.replace("0.0.0.0", "localhost"); + if (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } + + return configValue.from().withValue(url).build(); + } +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPConfigSourceProvider.java b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPConfigSourceProvider.java index f0593f04df0077..7f2e25fb6fe082 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPConfigSourceProvider.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPConfigSourceProvider.java @@ -1,6 +1,5 @@ package io.quarkus.test.common.http; -import java.io.Serializable; import java.util.Collections; import java.util.Map; import java.util.Set; @@ -26,24 +25,18 @@ public class TestHTTPConfigSourceProvider implements ConfigSourceProvider { static final String TEST_MANAGEMENT_URL_SSL_KEY = "test.management.url.ssl"; static final Map<String, String> entries = Map.of( - TEST_URL_KEY, sanitizeURL(TEST_URL_VALUE), - TEST_URL_SSL_KEY, sanitizeURL(TEST_URL_SSL_VALUE), - TEST_MANAGEMENT_URL_KEY, sanitizeURL(TEST_MANAGEMENT_URL_VALUE), - TEST_MANAGEMENT_URL_SSL_KEY, sanitizeURL(TEST_MANAGEMENT_URL_SSL_VALUE), - "%dev." + TEST_URL_KEY, sanitizeURL( - "http://${quarkus.http.host:localhost}:${quarkus.http.test-port:8080}${quarkus.http.root-path:${quarkus.servlet.context-path:}}")); - - private static String sanitizeURL(String url) { - return url.replace("0.0.0.0", "localhost"); - } + TEST_URL_KEY, TEST_URL_VALUE, + TEST_URL_SSL_KEY, TEST_URL_SSL_VALUE, + TEST_MANAGEMENT_URL_KEY, TEST_MANAGEMENT_URL_VALUE, + TEST_MANAGEMENT_URL_SSL_KEY, TEST_MANAGEMENT_URL_SSL_VALUE, + "%dev." + TEST_URL_KEY, + "http://${quarkus.http.host:localhost}:${quarkus.http.test-port:8080}${quarkus.http.root-path:${quarkus.servlet.context-path:}}"); public Iterable<ConfigSource> getConfigSources(final ClassLoader forClassLoader) { return Collections.singletonList(new TestURLConfigSource()); } - static class TestURLConfigSource implements ConfigSource, Serializable { - private static final long serialVersionUID = 4841094273900625000L; - + static class TestURLConfigSource implements ConfigSource { public Map<String, String> getProperties() { return entries; } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java index 87b1e87ad91b6b..af8539870aef51 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/http/TestHTTPResourceManager.java @@ -17,7 +17,7 @@ public class TestHTTPResourceManager { public static String getUri() { try { - return sanitizeUri(ConfigProvider.getConfig().getValue("test.url", String.class)); + return ConfigProvider.getConfig().getValue("test.url", String.class); } catch (IllegalStateException e) { //massive hack for dev mode tests, dev mode has not started yet //so we don't have any way to load this correctly from config @@ -27,7 +27,7 @@ public static String getUri() { public static String getManagementUri() { try { - return sanitizeUri(ConfigProvider.getConfig().getValue("test.management.url", String.class)); + return ConfigProvider.getConfig().getValue("test.management.url", String.class); } catch (IllegalStateException e) { //massive hack for dev mode tests, dev mode has not started yet //so we don't have any way to load this correctly from config @@ -36,26 +36,19 @@ public static String getManagementUri() { } public static String getSslUri() { - return sanitizeUri(ConfigProvider.getConfig().getValue("test.url.ssl", String.class)); + return ConfigProvider.getConfig().getValue("test.url.ssl", String.class); } public static String getManagementSslUri() { - return sanitizeUri(ConfigProvider.getConfig().getValue("test.management.url.ssl", String.class)); - } - - private static String sanitizeUri(String result) { - if ((result != null) && result.endsWith("/")) { - return result.substring(0, result.length() - 1); - } - return result; + return ConfigProvider.getConfig().getValue("test.management.url.ssl", String.class); } public static String getUri(RunningQuarkusApplication application) { - return sanitizeUri(application.getConfigValue("test.url", String.class).get()); + return application.getConfigValue("test.url", String.class).get(); } public static String getSslUri(RunningQuarkusApplication application) { - return sanitizeUri(application.getConfigValue("test.url.ssl", String.class).get()); + return application.getConfigValue("test.url.ssl", String.class).get(); } public static void inject(Object testCase) { diff --git a/test-framework/common/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor b/test-framework/common/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor new file mode 100644 index 00000000000000..ebc40d38477308 --- /dev/null +++ b/test-framework/common/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor @@ -0,0 +1 @@ +io.quarkus.test.common.http.TestHTTPConfigSourceInterceptor diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java index a7bf610f10f448..9254f46e1d2712 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java @@ -4,6 +4,7 @@ import static io.quarkus.test.common.PathTestHelper.getAppClassLocationForTestLocation; import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; +import java.io.FileOutputStream; import java.io.IOException; import java.lang.annotation.Annotation; import java.nio.file.Files; @@ -15,6 +16,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Properties; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -40,7 +42,6 @@ import io.quarkus.runtime.LaunchMode; import io.quarkus.test.common.PathTestHelper; import io.quarkus.test.common.QuarkusTestResource; -import io.quarkus.test.common.RestorableSystemProperties; import io.quarkus.test.common.TestClassIndexer; import io.quarkus.test.common.WithTestResource; @@ -147,31 +148,59 @@ protected PrepareResult createAugmentor(ExtensionContext context, Class<? extend // clear the test.url system property as the value leaks into the run when using different profiles System.clearProperty("test.url"); - Map<String, String> additional = new HashMap<>(); QuarkusTestProfile profileInstance = null; if (profile != null) { profileInstance = profile.getConstructor().newInstance(); - additional.putAll(profileInstance.getConfigOverrides()); + + Map<String, String> overrides = new HashMap<>(profileInstance.getConfigOverrides()); if (!profileInstance.getEnabledAlternatives().isEmpty()) { - additional.put("quarkus.arc.selected-alternatives", profileInstance.getEnabledAlternatives().stream() - .peek((c) -> { - if (!c.isAnnotationPresent(Alternative.class)) { - throw new RuntimeException( - "Enabled alternative " + c + " is not annotated with @Alternative"); - } - }) - .map(Class::getName).collect(Collectors.joining(","))); + overrides.put("quarkus.arc.selected-alternatives", + profileInstance.getEnabledAlternatives() + .stream() + .peek((c) -> { + if (!c.isAnnotationPresent(Alternative.class)) { + throw new RuntimeException( + "Enabled alternative " + c + " is not annotated with " + + "@Alternative"); + } + }) + .map(Class::getName) + .collect(Collectors.joining(","))); } if (profileInstance.disableApplicationLifecycleObservers()) { - additional.put("quarkus.arc.test.disable-application-lifecycle-observers", "true"); + overrides.put("quarkus.arc.test.disable-application-lifecycle-observers", "true"); } if (profileInstance.getConfigProfile() != null) { - additional.put(LaunchMode.TEST.getProfileKey(), profileInstance.getConfigProfile()); + overrides.put(LaunchMode.TEST.getProfileKey(), profileInstance.getConfigProfile()); + } + + // Creates a temporary application.properties file for the test with a high ordinal (build and runtime) + Path tempDirectory = Files.createTempDirectory(testClassLocation, requiredTestClass.getSimpleName()); + Path propertiesFile = tempDirectory.resolve("application.properties"); + Files.createFile(propertiesFile); + Properties properties = new Properties(); + // TODO - radcortez - This should be higher that system properties, but configuration like ports is being + // passed around using system properties, meaning that it cannot be overridden. We cannot use system + // properties to carry that information. See io.quarkus.vertx.http.runtime.PortSystemProperties + properties.put("config_ordinal", "399"); + properties.putAll(overrides); + try (FileOutputStream outputStream = new FileOutputStream(propertiesFile.toFile())) { + properties.store(outputStream, ""); } - //we just use system properties for now - //it's a lot simpler - shutdownTasks.add(RestorableSystemProperties.setProperties(additional)::close); + addToBuilderIfConditionMet.accept(tempDirectory); + + shutdownTasks.add(new Runnable() { + @Override + public void run() { + try { + Files.deleteIfExists(propertiesFile); + Files.deleteIfExists(tempDirectory); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); } CuratedApplication curatedApplication; diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java index d793d8e74b6c46..744e35bc67d0ad 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java @@ -77,9 +77,8 @@ private void ensurePrepared(ExtensionContext extensionContext, Class<? extends Q prepareResult = null; } if (prepareResult == null) { - final LinkedBlockingDeque<Runnable> shutdownTasks = new LinkedBlockingDeque<>(); - PrepareResult result = createAugmentor(extensionContext, profile, shutdownTasks); - prepareResult = result; + LinkedBlockingDeque<Runnable> shutdownTasks = new LinkedBlockingDeque<>(); + prepareResult = createAugmentor(extensionContext, profile, shutdownTasks); } } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index 0f881f19022ce2..915f33f98eda12 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -205,12 +205,12 @@ public Thread newThread(Runnable r) { } hangTimeout = new DurationConverter().convert(time); hangTaskKey = hangDetectionExecutor.schedule(hangDetectionTask, hangTimeout.toMillis(), TimeUnit.MILLISECONDS); - quarkusTestProfile = profile; + + LinkedBlockingDeque<Runnable> shutdownTasks = new LinkedBlockingDeque<>(); Class<?> requiredTestClass = context.getRequiredTestClass(); Closeable testResourceManager = null; try { - final LinkedBlockingDeque<Runnable> shutdownTasks = new LinkedBlockingDeque<>(); PrepareResult result = createAugmentor(context, profile, shutdownTasks); AugmentAction augmentAction = result.augmentAction; QuarkusTestProfile profileInstance = result.profileInstance; @@ -298,8 +298,7 @@ public void close() throws IOException { } } }; - ExtensionState state = new ExtensionState(testResourceManager, shutdownTask); - return state; + return new ExtensionState(testResourceManager, shutdownTask); } catch (Throwable e) { if (!InitialConfigurator.DELAYED_HANDLER.isActivated()) { activateLogging(); @@ -315,6 +314,16 @@ public void close() throws IOException { effectiveException.addSuppressed(determineEffectiveException(ex)); } + while (!shutdownTasks.isEmpty()) { + shutdownTasks.pop().run(); + } + + try { + TestClassIndexer.removeIndex(requiredTestClass); + } catch (Exception ignored) { + + } + throw effectiveException; } finally { if (originalCl != null) {