diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 130660b300b42..495652aacd6a4 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -178,9 +178,10 @@ jobs: uses: actions/upload-artifact@v2 if: ${{ failure() || cancelled() }} with: - name: "build-reports-JVM Tests - JDK ${{matrix.java.name}}" + name: "build-reports-Initial JDK 11 Build" path: | target/build-report.json + LICENSE.txt retention-days: 2 calculate-test-jobs: @@ -322,6 +323,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 - name: Upload gc.log uses: actions/upload-artifact@v2 @@ -388,6 +390,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 gradle-tests: @@ -447,6 +450,7 @@ jobs: **/build/test-results/test/TEST-*.xml **/target/*-reports/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 devtools-tests: @@ -506,6 +510,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 tcks-test: @@ -564,6 +569,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 native-tests: @@ -643,4 +649,5 @@ jobs: **/target/*-reports/TEST-*.xml **/build/test-results/test/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 diff --git a/bom/application/pom.xml b/bom/application/pom.xml index c101beff0e49f..fdaef17a893fb 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -47,14 +47,14 @@ 3.1.1 3.0.1 2.1.10 - 1.3.1 + 1.3.2 2.0.1 5.2.1 3.2.1 1.2.0 1.0.13 2.6.0 - 2.12.0 + 2.13.0 3.9.1 1.2.1 1.3.5 @@ -110,7 +110,7 @@ 1.16.1.Final 1.8.7.Final 3.4.2.Final - 4.1.2 + 4.1.3 4.5.13 4.4.14 4.1.4 @@ -121,7 +121,7 @@ 2.7.4 8.0.26 7.2.2.jre8 - 21.1.0.0 + 21.3.0.0 10.14.2.0 11.5.6.0 1.2.6 @@ -132,7 +132,7 @@ 12.1.7.Final 4.4.1.Final 2.9.2 - 4.1.65.Final + 4.1.67.Final 1.0.3 3.4.2.Final 1.0.0 @@ -170,14 +170,14 @@ 3.14.9 5.0.1 0.1.0 - 5.7.0 + 5.7.2 2.2.0 5.2.SP4 2.1.SP2 5.2.Final 2.1.SP1 3.11.2 - 5.3.1 + 5.8.0 4.8 1.1.4.Final 14.0.0 @@ -353,6 +353,15 @@ import + + + com.oracle.database.jdbc + ojdbc-bom + ${oracle-jdbc.version} + pom + import + + io.smallrye.reactive @@ -1608,6 +1617,11 @@ quarkus-mailer ${project.version} + + io.quarkus + quarkus-mailer-deployment + ${project.version} + io.quarkus quarkus-mongodb-client @@ -4489,11 +4503,6 @@ mssql-jdbc ${mssql-jdbc.version} - - com.oracle.database.jdbc - ojdbc8 - ${oracle-jdbc.version} - org.elasticsearch.client elasticsearch-rest-client diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ExcludeConfigBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ExcludeConfigBuildItem.java new file mode 100644 index 0000000000000..8c2cf955c2639 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ExcludeConfigBuildItem.java @@ -0,0 +1,41 @@ +package io.quarkus.deployment.builditem.nativeimage; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * A build item that allows extension to configure the native-image compiler to effectively + * ignore certain configuration files in specific jars. + * + * The {@code jarFile} property specifies the name of the jar file or a regular expression that can be used to + * match multiple jar files. + * Matching jar files using regular expressions should be done as a last resort. + * + * The {@code resourceName} property specifies the name of the resource file or a regular expression that can be used to + * match multiple resource files. + * For the match to work, the resources need to be part of the matched jar file(s) (see {@code jarFile}). + * Matching resource files using regular expressions should be done as a last resort. + * + * See https://github.com/oracle/graal/pull/3179 for more details. + */ +public final class ExcludeConfigBuildItem extends MultiBuildItem { + + private final String jarFile; + private final String resourceName; + + public ExcludeConfigBuildItem(String jarFile, String resourceName) { + this.jarFile = jarFile; + this.resourceName = resourceName; + } + + public ExcludeConfigBuildItem(String jarFile) { + this(jarFile, "/META-INF/native-image/native-image\\.properties"); + } + + public String getJarFile() { + return jarFile; + } + + public String getResourceName() { + return resourceName; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageAllowIncompleteClasspathAggregateBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageAllowIncompleteClasspathAggregateBuildItem.java new file mode 100644 index 0000000000000..147d85716e390 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageAllowIncompleteClasspathAggregateBuildItem.java @@ -0,0 +1,21 @@ +package io.quarkus.deployment.builditem.nativeimage; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Do not use directly: use {@see io.quarkus.deployment.builditem.nativeimage.NativeImageAllowIncompleteClasspathBuildItem} + * instead. + */ +public final class NativeImageAllowIncompleteClasspathAggregateBuildItem extends SimpleBuildItem { + + private final boolean allow; + + public NativeImageAllowIncompleteClasspathAggregateBuildItem(boolean allow) { + this.allow = allow; + } + + public boolean isAllow() { + return allow; + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageAllowIncompleteClasspathBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageAllowIncompleteClasspathBuildItem.java new file mode 100644 index 0000000000000..c0d6223607f54 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageAllowIncompleteClasspathBuildItem.java @@ -0,0 +1,32 @@ +package io.quarkus.deployment.builditem.nativeimage; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * If any build item of this type is produced, the native-image build tool + * will run with {@literal --allow-incomplete-classpath} set. + *

+ * This should be strongly discouraged as it makes diagnostics of any issue + * much more complex, and we have it seen affect error message of code + * seemingly unrelated to the code which is having the broken classpath. + *

+ * Use of this build item will trigger a warning during build. + * + * @Deprecated Please don't use it unless there is general consensus that we can't practically find a better solution. + */ +@Deprecated +public final class NativeImageAllowIncompleteClasspathBuildItem extends MultiBuildItem { + + private final String extensionName; + + /** + * @param extensionName Name the extension requiring this, so that it can be shamed appropriately during build. + */ + public NativeImageAllowIncompleteClasspathBuildItem(String extensionName) { + this.extensionName = extensionName; + } + + public String getExtensionName() { + return extensionName; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigMappingUtils.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigMappingUtils.java deleted file mode 100644 index 2cbe8dc5f7340..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigMappingUtils.java +++ /dev/null @@ -1,150 +0,0 @@ -package io.quarkus.deployment.configuration; - -import static io.quarkus.deployment.builditem.ConfigClassBuildItem.Type.MAPPING; -import static io.quarkus.deployment.builditem.ConfigClassBuildItem.Type.PROPERTIES; -import static java.util.Collections.emptySet; -import static org.eclipse.microprofile.config.inject.ConfigProperties.UNCONFIGURED_PREFIX; -import static org.jboss.jandex.AnnotationTarget.Kind.CLASS; -import static org.jboss.jandex.AnnotationTarget.Kind.FIELD; -import static org.jboss.jandex.AnnotationTarget.Kind.METHOD_PARAMETER; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationTarget; -import org.jboss.jandex.AnnotationValue; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.ClassType; -import org.jboss.jandex.DotName; -import org.jboss.jandex.MethodInfo; - -import io.quarkus.deployment.annotations.BuildProducer; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.ConfigClassBuildItem; -import io.quarkus.deployment.builditem.GeneratedClassBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.smallrye.config.ConfigMapping; -import io.smallrye.config.ConfigMappingLoader; -import io.smallrye.config.ConfigMappingMetadata; - -public class ConfigMappingUtils { - public static final DotName CONFIG_MAPPING_NAME = DotName.createSimple(ConfigMapping.class.getName()); - - private ConfigMappingUtils() { - } - - @BuildStep - public static void generateConfigClasses( - CombinedIndexBuildItem combinedIndex, - BuildProducer generatedClasses, - BuildProducer reflectiveClasses, - BuildProducer configClasses, - DotName configAnnotation) { - - for (AnnotationInstance instance : combinedIndex.getIndex().getAnnotations(configAnnotation)) { - AnnotationTarget target = instance.target(); - AnnotationValue annotationPrefix = instance.value("prefix"); - - if (target.kind().equals(FIELD)) { - if (annotationPrefix != null && !annotationPrefix.asString().equals(UNCONFIGURED_PREFIX)) { - configClasses.produce( - toConfigClassBuildItem(instance, toClass(target.asField().type().name()), - annotationPrefix.asString())); - continue; - } - } - - if (target.kind().equals(METHOD_PARAMETER)) { - if (annotationPrefix != null && !annotationPrefix.asString().equals(UNCONFIGURED_PREFIX)) { - ClassType classType = target.asMethodParameter().method().parameters() - .get(target.asMethodParameter().position()).asClassType(); - configClasses - .produce(toConfigClassBuildItem(instance, toClass(classType.name()), annotationPrefix.asString())); - continue; - } - } - - if (!target.kind().equals(CLASS)) { - continue; - } - - Class configClass = toClass(target.asClass().name()); - String prefix = Optional.ofNullable(annotationPrefix).map(AnnotationValue::asString).orElse(""); - - List configMappingsMetadata = ConfigMappingLoader.getConfigMappingsMetadata(configClass); - Set generatedClassesNames = new HashSet<>(); - Set mappingsInfo = new HashSet<>(); - configMappingsMetadata.forEach(mappingMetadata -> { - generatedClasses.produce( - new GeneratedClassBuildItem(true, mappingMetadata.getClassName(), mappingMetadata.getClassBytes())); - reflectiveClasses - .produce(ReflectiveClassBuildItem.builder(mappingMetadata.getInterfaceType()).methods(true).build()); - reflectiveClasses - .produce(ReflectiveClassBuildItem.builder(mappingMetadata.getClassName()).constructors(true).build()); - - for (Class parent : getHierarchy(mappingMetadata.getInterfaceType())) { - reflectiveClasses.produce(ReflectiveClassBuildItem.builder(parent).methods(true).build()); - } - - generatedClassesNames.add(mappingMetadata.getClassName()); - - ClassInfo mappingInfo = combinedIndex.getIndex() - .getClassByName(DotName.createSimple(mappingMetadata.getInterfaceType().getName())); - if (mappingInfo != null) { - mappingsInfo.add(mappingInfo); - } - }); - - // For implicit converters - for (ClassInfo classInfo : mappingsInfo) { - for (MethodInfo method : classInfo.methods()) { - reflectiveClasses.produce(new ReflectiveClassBuildItem(true, false, method.returnType().name().toString())); - } - } - - configClasses.produce(toConfigClassBuildItem(instance, configClass, generatedClassesNames, prefix)); - } - } - - private static Class toClass(DotName dotName) { - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - try { - return classLoader.loadClass(dotName.toString()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("The class (" + dotName.toString() + ") cannot be created during deployment.", e); - } - } - - private static ConfigClassBuildItem toConfigClassBuildItem( - AnnotationInstance instance, - Class configClass, - String prefix) { - return toConfigClassBuildItem(instance, configClass, emptySet(), prefix); - } - - private static ConfigClassBuildItem toConfigClassBuildItem( - AnnotationInstance instance, - Class configClass, - Set generatedClasses, - String prefix) { - if (instance.name().equals(CONFIG_MAPPING_NAME)) { - return new ConfigClassBuildItem(configClass, generatedClasses, prefix, MAPPING); - } else { - return new ConfigClassBuildItem(configClass, generatedClasses, prefix, PROPERTIES); - } - } - - private static List> getHierarchy(Class mapping) { - List> interfaces = new ArrayList<>(); - for (Class i : mapping.getInterfaces()) { - interfaces.add(i); - interfaces.addAll(getHierarchy(i)); - } - return interfaces; - } -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java index 377bac70e18c2..ee5ebd04d064d 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java @@ -70,7 +70,6 @@ import io.quarkus.runtime.configuration.RuntimeConfigSource; import io.quarkus.runtime.configuration.RuntimeConfigSourceFactory; import io.quarkus.runtime.configuration.RuntimeConfigSourceProvider; -import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; import io.smallrye.config.Converters; import io.smallrye.config.PropertiesConfigSource; import io.smallrye.config.SmallRyeConfig; @@ -175,8 +174,6 @@ public final class RunTimeConfigurationGenerator { static final MethodDescriptor CU_ADD_SOURCE_FACTORY_PROVIDER = MethodDescriptor.ofMethod(ConfigUtils.class, "addSourceFactoryProvider", void.class, SmallRyeConfigBuilder.class, ConfigSourceFactoryProvider.class); - static final MethodDescriptor CU_WITH_MAPPING = MethodDescriptor.ofMethod(ConfigUtils.class, "addMapping", - void.class, SmallRyeConfigBuilder.class, String.class, String.class); static final MethodDescriptor RCS_NEW = MethodDescriptor.ofConstructor(RuntimeConfigSource.class, String.class); static final MethodDescriptor RCSP_NEW = MethodDescriptor.ofConstructor(RuntimeConfigSourceProvider.class, String.class); @@ -301,7 +298,6 @@ public static final class GenerateOperation implements AutoCloseable { final Set runtimeConfigSources; final Set runtimeConfigSourceProviders; final Set runtimeConfigSourceFactories; - final Set configMappings; /** * Regular converters organized by type. Each converter is stored in a separate field. Some are used * only at build time, some only at run time, and some at both times. @@ -339,7 +335,6 @@ public static final class GenerateOperation implements AutoCloseable { runtimeConfigSources = builder.getRuntimeConfigSources(); runtimeConfigSourceProviders = builder.getRuntimeConfigSourceProviders(); runtimeConfigSourceFactories = builder.getRuntimeConfigSourceFactories(); - configMappings = builder.getConfigMappings(); cc = ClassCreator.builder().classOutput(classOutput).className(CONFIG_CLASS_NAME).setFinal(true).build(); generateEmptyParsers(cc); // not instantiable @@ -428,11 +423,6 @@ public static final class GenerateOperation implements AutoCloseable { clinit.invokeStaticMethod(CU_ADD_SOURCE_FACTORY_PROVIDER, buildTimeBuilder, clinit.newInstance(RCSF_NEW, clinit.load(discoveredConfigSourceFactory))); } - // add mappings - for (ConfigClassWithPrefix configMapping : configMappings) { - clinit.invokeStaticMethod(CU_WITH_MAPPING, buildTimeBuilder, - clinit.load(configMapping.getKlass().getName()), clinit.load(configMapping.getPrefix())); - } clinitConfig = clinit.checkCast(clinit.invokeVirtualMethod(SRCB_BUILD, buildTimeBuilder), SmallRyeConfig.class); @@ -676,12 +666,6 @@ public void run() { readConfig.newInstance(RCSF_NEW, readConfig.load(discoveredConfigSourceFactory))); } - // add mappings - for (ConfigClassWithPrefix configMapping : configMappings) { - readConfig.invokeStaticMethod(CU_WITH_MAPPING, runTimeBuilder, - readConfig.load(configMapping.getKlass().getName()), readConfig.load(configMapping.getPrefix())); - } - ResultHandle bootstrapConfig = null; if (bootstrapConfigSetupNeeded()) { bootstrapConfig = readBootstrapConfig.invokeVirtualMethod(SRCB_BUILD, bootstrapBuilder); @@ -1703,8 +1687,6 @@ public static final class Builder { private Set runtimeConfigSourceProviders; private Set runtimeConfigSourceFactories; - private Set configMappings; - Builder() { } @@ -1816,15 +1798,6 @@ public Builder setRuntimeConfigSourceFactories(final Set runtimeConfigSo return this; } - Set getConfigMappings() { - return configMappings; - } - - public Builder setConfigMappings(final Set configMappings) { - this.configMappings = configMappings; - return this; - } - public GenerateOperation build() { return new GenerateOperation(this); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java index f3ebf58184b3c..1c6f5009c660c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java @@ -73,8 +73,10 @@ import io.quarkus.deployment.QuarkusClassWriter; import io.quarkus.deployment.dev.ClassScanResult; import io.quarkus.deployment.dev.DevModeContext; +import io.quarkus.deployment.dev.RuntimeUpdatesProcessor; import io.quarkus.deployment.util.IoUtil; import io.quarkus.dev.console.QuarkusConsole; +import io.quarkus.dev.testing.TestWatchedFiles; import io.quarkus.dev.testing.TracingHandler; /** @@ -379,6 +381,11 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e QuarkusConsole.INSTANCE.setOutputFilter(null); + //this has to happen before notifying the listeners + Map watched = TestWatchedFiles.retrieveWatchedFilePaths(); + if (watched != null) { + RuntimeUpdatesProcessor.INSTANCE.setWatchedFilePaths(watched, true); + } for (TestRunListener listener : listeners) { listener.runComplete(new TestRunResults(runId, classScanResult, classScanResult == null, start, System.currentTimeMillis(), toResultsMap(testState.getCurrentResults()))); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunner.java index 3c057a5422295..cdbe69cf0fdd4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunner.java @@ -24,8 +24,6 @@ import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.deployment.dev.ClassScanResult; import io.quarkus.deployment.dev.DevModeContext; -import io.quarkus.deployment.dev.RuntimeUpdatesProcessor; -import io.quarkus.dev.testing.TestWatchedFiles; import io.quarkus.runtime.configuration.HyphenateEnumConverter; public class TestRunner { @@ -254,10 +252,6 @@ public void noTests(TestRunResults results) { synchronized (this) { runner = null; } - Map watched = TestWatchedFiles.retrieveWatchedFilePaths(); - if (watched != null) { - RuntimeUpdatesProcessor.INSTANCE.setWatchedFilePaths(watched, true); - } if (disabled) { return; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index 2210a067887cb..75ba060b0e62c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -26,6 +26,8 @@ import io.quarkus.bootstrap.model.AppDependency; import io.quarkus.bootstrap.util.IoUtils; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.ExcludeConfigBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageAllowIncompleteClasspathAggregateBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSecurityProviderBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem; import io.quarkus.deployment.pkg.NativeConfig; @@ -81,7 +83,9 @@ ArtifactResultBuildItem nativeSourcesResult(NativeConfig nativeConfig, NativeImageSourceJarBuildItem nativeImageSourceJarBuildItem, OutputTargetBuildItem outputTargetBuildItem, PackageConfig packageConfig, - List nativeImageProperties) { + List nativeImageProperties, + List excludeConfigs, + NativeImageAllowIncompleteClasspathAggregateBuildItem incompleteClassPathAllowed) { Path outputDir; try { @@ -100,6 +104,8 @@ ArtifactResultBuildItem nativeSourcesResult(NativeConfig nativeConfig, .setNativeConfig(nativeConfig) .setOutputTargetBuildItem(outputTargetBuildItem) .setNativeImageProperties(nativeImageProperties) + .setBrokenClasspath(incompleteClassPathAllowed.isAllow()) + .setExcludeConfigs(excludeConfigs) .setOutputDir(outputDir) .setRunnerJarName(runnerJar.getFileName().toString()) // the path to native-image is not known now, it is only known at the time the native-sources will be consumed @@ -130,6 +136,8 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa PackageConfig packageConfig, CurateOutcomeBuildItem curateOutcomeBuildItem, List nativeImageProperties, + List excludeConfigs, + NativeImageAllowIncompleteClasspathAggregateBuildItem incompleteClassPathAllowed, List nativeImageSecurityProviders, Optional processInheritIODisabled) { if (nativeConfig.debug.enabled) { @@ -187,6 +195,8 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, NativeImageSourceJa .setNativeConfig(nativeConfig) .setOutputTargetBuildItem(outputTargetBuildItem) .setNativeImageProperties(nativeImageProperties) + .setExcludeConfigs(excludeConfigs) + .setBrokenClasspath(incompleteClassPathAllowed.isAllow()) .setNativeImageSecurityProviders(nativeImageSecurityProviders) .setOutputDir(outputDir) .setRunnerJarName(runnerJarName) @@ -477,6 +487,7 @@ static class Builder { private NativeConfig nativeConfig; private OutputTargetBuildItem outputTargetBuildItem; private List nativeImageProperties; + private List excludeConfigs; private List nativeImageSecurityProviders; private Path outputDir; private String runnerJarName; @@ -484,6 +495,7 @@ static class Builder { private boolean isContainerBuild = false; private GraalVM.Version graalVMVersion = GraalVM.Version.UNVERSIONED; private String nativeImageName; + private boolean classpathIsBroken; public Builder setNativeConfig(NativeConfig nativeConfig) { this.nativeConfig = nativeConfig; @@ -500,6 +512,16 @@ public Builder setNativeImageProperties(List return this; } + public Builder setBrokenClasspath(boolean classpathIsBroken) { + this.classpathIsBroken = classpathIsBroken; + return this; + } + + public Builder setExcludeConfigs(List excludeConfigs) { + this.excludeConfigs = excludeConfigs; + return this; + } + public Builder setNativeImageSecurityProviders( List nativeImageSecurityProviders) { this.nativeImageSecurityProviders = nativeImageSecurityProviders; @@ -584,8 +606,6 @@ public NativeImageInvokerInfo build() { "-H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime"); //the default collection policy results in full GC's 50% of the time nativeImageArgs.add("-H:+JNI"); nativeImageArgs.add("-H:+AllowFoldMethods"); - nativeImageArgs.add("-jar"); - nativeImageArgs.add(runnerJarName); if (nativeConfig.enableFallbackImages) { nativeImageArgs.add("-H:FallbackThreshold=5"); @@ -595,6 +615,10 @@ public NativeImageInvokerInfo build() { nativeImageArgs.add("-H:FallbackThreshold=0"); } + if (classpathIsBroken) { + nativeImageArgs.add("--allow-incomplete-classpath"); + } + if (nativeConfig.reportErrorsAtRuntime) { nativeImageArgs.add("-H:+ReportUnsupportedElementsAtRuntime"); } @@ -700,10 +724,21 @@ public NativeImageInvokerInfo build() { .collect(Collectors.joining(",")); nativeImageArgs.add("-H:AdditionalSecurityProviders=" + additionalSecurityProviders); } + + // --exclude-config options + for (ExcludeConfigBuildItem excludeConfig : excludeConfigs) { + nativeImageArgs.add("--exclude-config"); + nativeImageArgs.add(excludeConfig.getJarFile()); + nativeImageArgs.add(excludeConfig.getResourceName()); + } } nativeImageArgs.add(nativeImageName); + //Make sure to have the -jar as last one, as it otherwise breaks "--exclude-config" + nativeImageArgs.add("-jar"); + nativeImageArgs.add(runnerJarName); + return new NativeImageInvokerInfo(nativeImageArgs); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java index 76baaac11caab..0eacb06801099 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java @@ -2,9 +2,7 @@ import static io.quarkus.deployment.steps.ConfigBuildSteps.SERVICES_PREFIX; import static io.quarkus.deployment.util.ServiceUtil.classNamesNamedIn; -import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; import java.io.IOException; import java.lang.reflect.Modifier; @@ -31,7 +29,6 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.AdditionalBootstrapConfigSourceProviderBuildItem; import io.quarkus.deployment.builditem.AdditionalStaticInitConfigSourceProviderBuildItem; -import io.quarkus.deployment.builditem.ConfigClassBuildItem; import io.quarkus.deployment.builditem.ConfigurationBuildItem; import io.quarkus.deployment.builditem.ConfigurationTypeBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; @@ -54,7 +51,6 @@ import io.quarkus.runtime.configuration.ConfigChangeRecorder; import io.quarkus.runtime.configuration.ConfigurationRuntimeConfig; import io.quarkus.runtime.configuration.RuntimeOverrideConfigSource; -import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; import io.smallrye.config.ConfigSourceFactory; import io.smallrye.config.PropertiesLocationConfigSourceFactory; @@ -92,8 +88,7 @@ void generateConfigClass( LiveReloadBuildItem liveReloadBuildItem, List additionalBootstrapConfigSourceProviders, List staticInitConfigSourceProviders, - List staticInitConfigSourceFactories, - List configClasses) + List staticInitConfigSourceFactories) throws IOException { if (liveReloadBuildItem.isLiveReload()) { @@ -135,7 +130,6 @@ void generateConfigClass( .setRuntimeConfigSources(discoveredConfigSources) .setRuntimeConfigSourceProviders(discoveredConfigSourceProviders) .setRuntimeConfigSourceFactories(discoveredConfigSourceFactories) - .setConfigMappings(getConfigClassesWithPrefix(configClasses)) .build() .run(); } @@ -249,11 +243,4 @@ private static Set staticSafeServices(Set services) { } return staticSafe; } - - private static Set getConfigClassesWithPrefix(List configClasses) { - return configClasses.stream() - .filter(ConfigClassBuildItem::isMapping) - .map(configMapping -> configClassWithPrefix(configMapping.getConfigClass(), configMapping.getPrefix())) - .collect(toSet()); - } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageAllowIncompleteClasspathAggregateStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageAllowIncompleteClasspathAggregateStep.java new file mode 100644 index 0000000000000..d30507eaad93a --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageAllowIncompleteClasspathAggregateStep.java @@ -0,0 +1,38 @@ +package io.quarkus.deployment.steps; + +import java.util.List; +import java.util.stream.Collectors; + +import org.jboss.logging.Logger; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.NativeImageAllowIncompleteClasspathAggregateBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageAllowIncompleteClasspathBuildItem; + +@SuppressWarnings("deprecation") +public final class NativeImageAllowIncompleteClasspathAggregateStep { + + private static final Logger log = Logger.getLogger(NativeImageAllowIncompleteClasspathAggregateStep.class); + + @BuildStep + NativeImageAllowIncompleteClasspathAggregateBuildItem aggregateIndividualItems( + List list) { + if (list.isEmpty()) { + return new NativeImageAllowIncompleteClasspathAggregateBuildItem(false); + } else { + final String extensionsRequiringBrokenClasspath = list.stream() + .map(NativeImageAllowIncompleteClasspathBuildItem::getExtensionName) + .collect(Collectors.joining(",")); + log.warn("The following extensions have required the '--allow-incomplete-classpath' flag to be set: {" + + extensionsRequiringBrokenClasspath + + "}. This is a global flag which might have unexpected effects on other extensions as well, and is a hint of the library " + + + "needing some additional refactoring to better support GraalVM native-image. In the case of 3rd party dependencies and/or" + + + " proprietary code there is not much we can do - please ask for support to your library vendor." + + " If you incur in any problem with other Quarkus extensions, please try reproducing the problem without these extensions first."); + return new NativeImageAllowIncompleteClasspathAggregateBuildItem(true); + } + } + +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java index b362ff6e39826..c9d2c35e3088c 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigUtils.java @@ -191,15 +191,6 @@ public static void addSourceFactoryProvider(SmallRyeConfigBuilder builder, Confi builder.withSources(provider.getConfigSourceFactory(Thread.currentThread().getContextClassLoader())); } - public static void addMapping(SmallRyeConfigBuilder builder, String mappingClass, String prefix) { - ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); - try { - builder.withMapping(contextClassLoader.loadClass(mappingClass), prefix); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - /** * Checks if a property is present in the current Configuration. * diff --git a/devtools/cli/src/main/java/io/quarkus/cli/common/TargetQuarkusVersionGroup.java b/devtools/cli/src/main/java/io/quarkus/cli/common/TargetQuarkusVersionGroup.java index 5e3b2a1254a62..11c55b6fe0a9b 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/common/TargetQuarkusVersionGroup.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/common/TargetQuarkusVersionGroup.java @@ -2,13 +2,15 @@ import io.quarkus.cli.Version; import io.quarkus.maven.ArtifactCoords; -import io.quarkus.maven.StreamCoords; import io.quarkus.platform.tools.ToolsConstants; +import io.quarkus.registry.catalog.PlatformStreamCoords; import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; public class TargetQuarkusVersionGroup { - StreamCoords streamCoords = null; + final static String FULL_EXAMPLE = ToolsConstants.DEFAULT_PLATFORM_BOM_GROUP_ID + ":" + + ToolsConstants.DEFAULT_PLATFORM_BOM_ARTIFACT_ID + ":2.2.0.Final"; + PlatformStreamCoords streamCoords = null; String validStream = null; ArtifactCoords platformBom = null; @@ -18,12 +20,12 @@ public class TargetQuarkusVersionGroup { CommandSpec spec; @CommandLine.Option(paramLabel = "platformKey:streamId", names = { "-S", - "--stream" }, description = "A target stream, for example:%n io.quarkus.platform:999-SNAPSHOT%n io.quarkus.platform:2.0") + "--stream" }, description = "A target stream, for example:%n io.quarkus.platform:2.0") void setStream(String stream) { stream = stream.trim(); if (!stream.isEmpty()) { try { - streamCoords = StreamCoords.fromString(stream); + streamCoords = PlatformStreamCoords.fromString(stream); validStream = stream; } catch (IllegalArgumentException iex) { throw new CommandLine.ParameterException(spec.commandLine(), @@ -34,7 +36,12 @@ void setStream(String stream) { } @CommandLine.Option(paramLabel = "groupId:artifactId:version", names = { "-P", - "--platform-bom" }, description = "A specific Quarkus platform BOM, for example:%n io.quarkus:quarkus-bom:2.0.0.Final") + "--platform-bom" }, description = "A specific Quarkus platform BOM, for example:%n" + + " " + FULL_EXAMPLE + "%n" + + " io.quarkus::999-SNAPSHOT" + + " 2.2.0.Final%n" + + "Default groupId: " + ToolsConstants.DEFAULT_PLATFORM_BOM_GROUP_ID + "%n" + + "Default artifactId: " + ToolsConstants.DEFAULT_PLATFORM_BOM_ARTIFACT_ID + "%n") void setPlatformBom(String bom) { bom = bom.replaceFirst("^::", "").trim(); if (!bom.isEmpty()) { @@ -87,7 +94,7 @@ public boolean isStreamSpecified() { return streamCoords != null; } - public StreamCoords getStream() { + public PlatformStreamCoords getStream() { return streamCoords; } diff --git a/devtools/cli/src/test/java/io/quarkus/cli/common/TargetQuarkusVersionGroupTest.java b/devtools/cli/src/test/java/io/quarkus/cli/common/TargetQuarkusVersionGroupTest.java index 9b0b62eb44fe1..e2d3f8593cc34 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/common/TargetQuarkusVersionGroupTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/common/TargetQuarkusVersionGroupTest.java @@ -5,8 +5,8 @@ import io.quarkus.cli.Version; import io.quarkus.maven.ArtifactCoords; -import io.quarkus.maven.StreamCoords; import io.quarkus.platform.tools.ToolsConstants; +import io.quarkus.registry.catalog.PlatformStreamCoords; public class TargetQuarkusVersionGroupTest { final static String clientVersion = Version.clientVersion(); @@ -64,7 +64,7 @@ void testPlatformUseDefaultGroupVersion() { void testStreamUseDFullyQualified() { qvg.setStream("stream-platform:stream-version"); - StreamCoords coords = qvg.getStream(); + PlatformStreamCoords coords = qvg.getStream(); Assertions.assertEquals("stream-platform", coords.getPlatformKey()); Assertions.assertEquals("stream-version", coords.getStreamId()); } @@ -73,7 +73,7 @@ void testStreamUseDFullyQualified() { void testStreamUseDefaultPlatformKey() { qvg.setStream(":stream-version"); - StreamCoords coords = qvg.getStream(); + PlatformStreamCoords coords = qvg.getStream(); Assertions.assertNull(coords.getPlatformKey()); Assertions.assertEquals("stream-version", coords.getStreamId()); } @@ -82,7 +82,7 @@ void testStreamUseDefaultPlatformKey() { void testStreamUseDefaultStreamId() { qvg.setStream("stream-platform:"); - StreamCoords coords = qvg.getStream(); + PlatformStreamCoords coords = qvg.getStream(); Assertions.assertEquals("stream-platform", coords.getPlatformKey()); Assertions.assertEquals("", coords.getStreamId()); } diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPlugin.java b/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPlugin.java index 0130082fdd675..c2f3d735004a5 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPlugin.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPlugin.java @@ -33,7 +33,6 @@ import io.quarkus.gradle.builder.QuarkusModelBuilder; import io.quarkus.gradle.dependency.ApplicationDeploymentClasspathBuilder; import io.quarkus.gradle.dependency.ConditionalDependenciesEnabler; -import io.quarkus.gradle.dependency.ExtensionDependency; import io.quarkus.gradle.extension.QuarkusPluginExtension; import io.quarkus.gradle.extension.SourceSetExtension; import io.quarkus.gradle.tasks.QuarkusAddExtension; @@ -121,7 +120,7 @@ private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) { Task quarkusBuild = tasks.create(QUARKUS_BUILD_TASK_NAME, QuarkusBuild.class); quarkusBuild.dependsOn(quarkusGenerateCode); - Task quarkusDev = tasks.create(QUARKUS_DEV_TASK_NAME, QuarkusDev.class); + QuarkusDev quarkusDev = tasks.create(QUARKUS_DEV_TASK_NAME, QuarkusDev.class); Task quarkusRemoteDev = tasks.create(QUARKUS_REMOTE_DEV_TASK_NAME, QuarkusRemoteDev.class); Task quarkusTest = tasks.create(QUARKUS_TEST_TASK_NAME, QuarkusTest.class); tasks.create(QUARKUS_TEST_CONFIG_TASK_NAME, QuarkusTestConfig.class); @@ -233,6 +232,7 @@ public void execute(Task test) { }); project.getPlugins().withId("org.jetbrains.kotlin.jvm", plugin -> { + quarkusDev.shouldPropagateJavaCompilerArgs(false); tasks.getByName("compileKotlin").dependsOn(quarkusGenerateCode); tasks.getByName("compileTestKotlin").dependsOn(quarkusGenerateCodeTests); }); @@ -272,15 +272,14 @@ private void afterEvaluate(Project project) { ConditionalDependenciesEnabler conditionalDependenciesEnabler = new ConditionalDependenciesEnabler(project); ApplicationDeploymentClasspathBuilder deploymentClasspathBuilder = new ApplicationDeploymentClasspathBuilder(project); - Set commonExtensions = conditionalDependenciesEnabler + conditionalDependenciesEnabler .declareConditionalDependencies(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME); - deploymentClasspathBuilder.createBuildClasspath(commonExtensions, JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME); - deploymentClasspathBuilder.addCommonExtension(commonExtensions); + deploymentClasspathBuilder.createBuildClasspath(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME, true); for (String baseConfiguration : CONDITIONAL_DEPENDENCY_LOOKUP) { - Set extensionDependencies = conditionalDependenciesEnabler + conditionalDependenciesEnabler .declareConditionalDependencies(baseConfiguration); - deploymentClasspathBuilder.createBuildClasspath(extensionDependencies, baseConfiguration); + deploymentClasspathBuilder.createBuildClasspath(baseConfiguration, false); } final HashSet visited = new HashSet<>(); diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java b/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java index e9e53d11a2e59..f49fe94a7138f 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java @@ -22,25 +22,25 @@ public static String toDeploymentConfigurationName(String baseConfigurationName) return baseConfigurationName + DEPLOYMENT_CONFIGURATION_SUFFIX; } - public void createBuildClasspath(Set extensions, String baseConfigurationName) { + public void createBuildClasspath(String baseConfigurationName, boolean common) { String deploymentConfigurationName = toDeploymentConfigurationName(baseConfigurationName); project.getConfigurations().create(deploymentConfigurationName); DependencyHandler dependencies = project.getDependencies(); - for (ExtensionDependency extension : extensions) { - if (commonExtensions.contains(extension)) { + Set firstLevelExtensions = DependencyUtils.loadQuarkusExtension(project, + project.getConfigurations().findByName(baseConfigurationName)); + for (ExtensionDependency extension : firstLevelExtensions) { + if (common) { + commonExtensions.add(extension); + } else if (commonExtensions.contains(extension)) { continue; } extension.createDeploymentVariant(dependencies); - createDeploymentClasspath(deploymentConfigurationName, extension, dependencies); + requireDeploymentDependency(deploymentConfigurationName, extension, dependencies); } } - public void addCommonExtension(Set commonExtensions) { - this.commonExtensions.addAll(commonExtensions); - } - - private void createDeploymentClasspath(String deploymentConfigurationName, ExtensionDependency extension, + private void requireDeploymentDependency(String deploymentConfigurationName, ExtensionDependency extension, DependencyHandler dependencies) { ExternalDependency dependency = (ExternalDependency) dependencies.add(deploymentConfigurationName, extension.asDependencyNotation()); diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java b/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java index fcb942ff7dbc9..39414d74335b7 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java @@ -1,66 +1,46 @@ package io.quarkus.gradle.dependency; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.Set; -import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.ModuleVersionIdentifier; import org.gradle.api.artifacts.ResolvedArtifact; -import io.quarkus.bootstrap.BootstrapConstants; -import io.quarkus.bootstrap.model.AppArtifactCoords; -import io.quarkus.bootstrap.model.AppArtifactKey; -import io.quarkus.bootstrap.util.BootstrapUtils; -import io.quarkus.bootstrap.util.ZipUtils; - public class ConditionalDependenciesEnabler { private final Map featureVariants = new HashMap<>(); + private final Project project; public ConditionalDependenciesEnabler(Project project) { this.project = project; } - public Set declareConditionalDependencies(String baseConfigurationName) { + public void declareConditionalDependencies(String baseConfigurationName) { featureVariants.clear(); Configuration resolvedConfiguration = DependencyUtils.duplicateConfiguration(project, project.getConfigurations().getByName(baseConfigurationName)); + Set runtimeArtifacts = resolvedConfiguration.getResolvedConfiguration().getResolvedArtifacts(); List extensions = collectExtensionsForResolution(runtimeArtifacts); featureVariants.putAll(extractFeatureVariants(extensions)); resolveConditionalDependencies(extensions, resolvedConfiguration, baseConfigurationName); - - Configuration resolvedExtensionsConfiguration = DependencyUtils.duplicateConfiguration(project, - project.getConfigurations().getByName(baseConfigurationName)); - Set enabledExtension = getEnabledExtension(resolvedExtensionsConfiguration); - project.getConfigurations().remove(resolvedExtensionsConfiguration); - - return enabledExtension; } private List collectExtensionsForResolution(Set runtimeArtifacts) { List firstLevelExtensions = new ArrayList<>(); for (ResolvedArtifact artifact : runtimeArtifacts) { - ExtensionDependency extension = getExtensionInfoOrNull(artifact); + ExtensionDependency extension = DependencyUtils.getExtensionInfoOrNull(project, artifact); if (extension != null) { if (!extension.conditionalDependencies.isEmpty()) { if (extension.needsResolution(runtimeArtifacts)) { @@ -72,43 +52,19 @@ private List collectExtensionsForResolution(Set getEnabledExtension(Configuration classpath) { - Set enabledExtensions = new HashSet<>(); - for (ResolvedArtifact artifact : classpath.getResolvedConfiguration().getResolvedArtifacts()) { - ExtensionDependency extension = getExtensionInfoOrNull(artifact); - if (extension != null) { - enabledExtensions.add(extension); - } - } - return enabledExtensions; - } - - private Map extractFeatureVariants(List extensions) { - Map possibleVariant = new HashMap<>(); - for (ExtensionDependency extension : extensions) { - for (Dependency dependency : extension.conditionalDependencies) { - possibleVariant.put(DependencyUtils.asFeatureName(dependency), extension); - } - } - return possibleVariant; - } - private void resolveConditionalDependencies(List conditionalExtensions, Configuration existingDependencies, String baseConfigurationName) { final Configuration conditionalDeps = createConditionalDependenciesConfiguration(existingDependencies, conditionalExtensions); boolean hasChanged = false; - Map validConditionalDependencies = new HashMap<>(); List newConditionalDependencies = new ArrayList<>(); newConditionalDependencies.addAll(conditionalExtensions); for (ResolvedArtifact artifact : conditionalDeps.getResolvedConfiguration().getResolvedArtifacts()) { - ExtensionDependency extensionDependency = getExtensionInfoOrNull(artifact); + ExtensionDependency extensionDependency = DependencyUtils.getExtensionInfoOrNull(project, artifact); if (extensionDependency != null) { if (DependencyUtils.exist(conditionalDeps.getResolvedConfiguration().getResolvedArtifacts(), extensionDependency.dependencyConditions)) { enableConditionalDependency(extensionDependency.extensionId); - validConditionalDependencies.put(DependencyUtils.asFeatureName(extensionDependency.extensionId), - extensionDependency); if (!extensionDependency.conditionalDependencies.isEmpty()) { featureVariants.putAll(extractFeatureVariants(Collections.singletonList(extensionDependency))); } @@ -130,6 +86,16 @@ private void resolveConditionalDependencies(List conditiona } } + private Map extractFeatureVariants(List extensions) { + Map possibleVariant = new HashMap<>(); + for (ExtensionDependency extension : extensions) { + for (Dependency dependency : extension.conditionalDependencies) { + possibleVariant.put(DependencyUtils.asFeatureName(dependency), extension); + } + } + return possibleVariant; + } + private Configuration createConditionalDependenciesConfiguration(Configuration existingDeps, List extensions) { Configuration newConfiguration = existingDeps.copy(); @@ -153,55 +119,4 @@ private void enableConditionalDependency(ModuleVersionIdentifier dependency) { } extension.importConditionalDependency(project.getDependencies(), dependency); } - - private ExtensionDependency getExtensionInfoOrNull(ResolvedArtifact artifact) { - ModuleVersionIdentifier artifactId = artifact.getModuleVersion().getId(); - File artifactFile = artifact.getFile(); - if (!artifactFile.exists() || !"jar".equals(artifact.getExtension())) { - return null; - } - if (artifactFile.isDirectory()) { - Path descriptorPath = artifactFile.toPath().resolve(BootstrapConstants.DESCRIPTOR_PATH); - if (Files.exists(descriptorPath)) { - return loadExtensionInfo(descriptorPath, artifactId); - } - } else { - try (FileSystem artifactFs = ZipUtils.newFileSystem(artifactFile.toPath())) { - Path descriptorPath = artifactFs.getPath(BootstrapConstants.DESCRIPTOR_PATH); - if (Files.exists(descriptorPath)) { - return loadExtensionInfo(descriptorPath, artifactId); - } - } catch (IOException e) { - throw new GradleException("Failed to read " + artifactFile, e); - } - } - return null; - } - - private ExtensionDependency loadExtensionInfo(Path descriptorPath, ModuleVersionIdentifier exentionId) { - final Properties extensionProperties = new Properties(); - try (BufferedReader reader = Files.newBufferedReader(descriptorPath)) { - extensionProperties.load(reader); - } catch (IOException e) { - throw new GradleException("Failed to load " + descriptorPath, e); - } - AppArtifactCoords deploymentModule = AppArtifactCoords - .fromString(extensionProperties.getProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT)); - final List conditionalDependencies; - if (extensionProperties.containsKey(BootstrapConstants.CONDITIONAL_DEPENDENCIES)) { - final String[] deps = BootstrapUtils - .splitByWhitespace(extensionProperties.getProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES)); - conditionalDependencies = new ArrayList<>(deps.length); - for (String conditionalDep : deps) { - conditionalDependencies.add(DependencyUtils.create(project.getDependencies(), conditionalDep)); - } - } else { - conditionalDependencies = Collections.emptyList(); - } - - final AppArtifactKey[] constraints = BootstrapUtils - .parseDependencyCondition(extensionProperties.getProperty(BootstrapConstants.DEPENDENCY_CONDITION)); - return new ExtensionDependency(exentionId, deploymentModule, conditionalDependencies, - constraints == null ? Collections.emptyList() : Arrays.asList(constraints)); - } } diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/DependencyUtils.java b/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/DependencyUtils.java index d10ba758e398c..91e17dbe0bf19 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/DependencyUtils.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/dependency/DependencyUtils.java @@ -1,10 +1,20 @@ package io.quarkus.gradle.dependency; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Properties; import java.util.Set; +import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.UnknownDomainObjectException; import org.gradle.api.artifacts.Configuration; @@ -18,8 +28,11 @@ import org.gradle.api.initialization.IncludedBuild; import org.gradle.api.plugins.JavaPlugin; +import io.quarkus.bootstrap.BootstrapConstants; import io.quarkus.bootstrap.model.AppArtifactCoords; import io.quarkus.bootstrap.model.AppArtifactKey; +import io.quarkus.bootstrap.util.BootstrapUtils; +import io.quarkus.bootstrap.util.ZipUtils; public class DependencyUtils { @@ -74,6 +87,21 @@ public static boolean exists(Set runtimeArtifacts, Dependency return false; } + public static Set loadQuarkusExtension(Project project, Configuration configuration) { + Set extensions = new HashSet<>(); + Configuration configurationCopy = duplicateConfiguration(project, configuration); + + Set resolvedArtifacts = configurationCopy.getResolvedConfiguration().getResolvedArtifacts(); + for (ResolvedArtifact artifact : resolvedArtifacts) { + ExtensionDependency extension = getExtensionInfoOrNull(project, artifact); + if (extension != null) { + extensions.add(extension); + } + } + + return extensions; + } + public static boolean isTestFixtureDependency(Dependency dependency) { if (!(dependency instanceof ModuleDependency)) { return false; @@ -138,4 +166,56 @@ public static String asFeatureName(ModuleVersionIdentifier version) { public static String asFeatureName(Dependency version) { return version.getGroup() + ":" + version.getName(); } + + public static ExtensionDependency getExtensionInfoOrNull(Project project, ResolvedArtifact artifact) { + ModuleVersionIdentifier artifactId = artifact.getModuleVersion().getId(); + File artifactFile = artifact.getFile(); + if (!artifactFile.exists() || !"jar".equals(artifact.getExtension())) { + return null; + } + if (artifactFile.isDirectory()) { + Path descriptorPath = artifactFile.toPath().resolve(BootstrapConstants.DESCRIPTOR_PATH); + if (Files.exists(descriptorPath)) { + return loadExtensionInfo(project, descriptorPath, artifactId); + } + } else { + try (FileSystem artifactFs = ZipUtils.newFileSystem(artifactFile.toPath())) { + Path descriptorPath = artifactFs.getPath(BootstrapConstants.DESCRIPTOR_PATH); + if (Files.exists(descriptorPath)) { + return loadExtensionInfo(project, descriptorPath, artifactId); + } + } catch (IOException e) { + throw new GradleException("Failed to read " + artifactFile, e); + } + } + return null; + } + + private static ExtensionDependency loadExtensionInfo(Project project, Path descriptorPath, + ModuleVersionIdentifier exentionId) { + final Properties extensionProperties = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(descriptorPath)) { + extensionProperties.load(reader); + } catch (IOException e) { + throw new GradleException("Failed to load " + descriptorPath, e); + } + AppArtifactCoords deploymentModule = AppArtifactCoords + .fromString(extensionProperties.getProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT)); + final List conditionalDependencies; + if (extensionProperties.containsKey(BootstrapConstants.CONDITIONAL_DEPENDENCIES)) { + final String[] deps = BootstrapUtils + .splitByWhitespace(extensionProperties.getProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES)); + conditionalDependencies = new ArrayList<>(deps.length); + for (String conditionalDep : deps) { + conditionalDependencies.add(DependencyUtils.create(project.getDependencies(), conditionalDep)); + } + } else { + conditionalDependencies = Collections.emptyList(); + } + + final AppArtifactKey[] constraints = BootstrapUtils + .parseDependencyCondition(extensionProperties.getProperty(BootstrapConstants.DEPENDENCY_CONDITION)); + return new ExtensionDependency(exentionId, deploymentModule, conditionalDependencies, + constraints == null ? Collections.emptyList() : Arrays.asList(constraints)); + } } diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java index 3c050bb07db45..f28c8bca01aa5 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java @@ -74,6 +74,8 @@ public class QuarkusDev extends QuarkusTask { private List compilerArgs = new LinkedList<>(); + private boolean shouldPropagateJavaCompilerArgs = true; + @Inject public QuarkusDev() { super("Development mode: enables hot deployment with background compilation"); @@ -303,7 +305,7 @@ private QuarkusDevModeLauncher newLauncher() throws Exception { builder.targetJavaVersion(javaPluginConvention.getTargetCompatibility().toString()); } - if (getCompilerArgs().isEmpty()) { + if (getCompilerArgs().isEmpty() && shouldPropagateJavaCompilerArgs) { getJavaCompileTask() .map(compileTask -> compileTask.getOptions().getCompilerArgs()) .ifPresent(builder::compilerOptions); @@ -571,4 +573,8 @@ private void addToClassPaths(GradleDevModeLauncher.Builder classPathManifest, Fi classPathManifest.classpathEntry(file); } } + + public void shouldPropagateJavaCompilerArgs(boolean shouldPropagateJavaCompilerArgs) { + this.shouldPropagateJavaCompilerArgs = shouldPropagateJavaCompilerArgs; + } } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/CheckForUpdatesMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/CheckForUpdatesMojo.java new file mode 100644 index 0000000000000..4fbf90c7fdc90 --- /dev/null +++ b/devtools/maven/src/main/java/io/quarkus/maven/CheckForUpdatesMojo.java @@ -0,0 +1,412 @@ +package io.quarkus.maven; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.maven.model.Dependency; +import org.apache.maven.model.Plugin; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.impl.RemoteRepositoryManager; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.resolution.ArtifactResult; + +import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; +import io.quarkus.devtools.commands.data.QuarkusCommandException; +import io.quarkus.devtools.project.QuarkusProjectHelper; +import io.quarkus.maven.utilities.MojoUtils; +import io.quarkus.platform.tools.maven.MojoMessageWriter; +import io.quarkus.registry.ExtensionCatalogResolver; +import io.quarkus.registry.RegistryResolutionException; +import io.quarkus.registry.catalog.Extension; +import io.quarkus.registry.catalog.ExtensionCatalog; +import io.quarkus.registry.catalog.ExtensionOrigin; +import io.quarkus.registry.catalog.selection.ExtensionOrigins; +import io.quarkus.registry.catalog.selection.OriginCombination; +import io.quarkus.registry.catalog.selection.OriginPreference; +import io.quarkus.registry.catalog.selection.OriginSelector; +import io.quarkus.registry.util.PlatformArtifacts; + +/** + * NOTE: this mojo is experimental + */ +@Mojo(name = "check-for-updates", requiresProject = true) +public class CheckForUpdatesMojo extends AbstractMojo { + + @Parameter(defaultValue = "${project}") + protected MavenProject project; + + @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true, required = true) + private List repos; + + @Parameter(defaultValue = "${repositorySystemSession}", readonly = true) + private RepositorySystemSession repoSession; + + @Component + private RepositorySystem repoSystem; + + @Component + RemoteRepositoryManager remoteRepoManager; + + @SuppressWarnings("unchecked") + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + + getLog().warn( + "This goal is experimental. Its name, parameters, output and implementation will be evolving without the promise of keeping backward compatibility"); + + if (project.getFile() == null) { + throw new MojoExecutionException("This goal requires a project"); + } + + if (!QuarkusProjectHelper.isRegistryClientEnabled()) { + throw new MojoExecutionException("This goal requires a Quarkus extension registry client to be enabled"); + } + + final Map previousExtensions = getDirectExtensionDependencies(); + if (previousExtensions.isEmpty()) { + getLog().info("The project does not appear to depend on any Quarkus extension directly"); + return; + } + + final MavenArtifactResolver mvn; + try { + mvn = MavenArtifactResolver.builder() + .setRepositorySystem(repoSystem) + .setRepositorySystemSession( + getLog().isDebugEnabled() ? repoSession : MojoUtils.muteTransferListener(repoSession)) + .setRemoteRepositories(repos) + .setRemoteRepositoryManager(remoteRepoManager) + .build(); + } catch (Exception e) { + throw new MojoExecutionException("Failed to initialize Maven artifact resolver", e); + } + final MojoMessageWriter log = new MojoMessageWriter(getLog()); + final ExtensionCatalogResolver catalogResolver; + try { + catalogResolver = QuarkusProjectHelper.getCatalogResolver(mvn, log); + } catch (RegistryResolutionException e) { + throw new MojoExecutionException("Failed to initialize Quarkus extension registry client", e); + } + + if (!catalogResolver.hasRegistries()) { + throw new MojoExecutionException("Configured Quarkus extension registries aren't available"); + } + + final ExtensionCatalog latestCatalog; + try { + latestCatalog = catalogResolver.resolveExtensionCatalog(); + } catch (RegistryResolutionException e) { + throw new MojoExecutionException( + "Failed to resolve the latest Quarkus extension catalog from the configured extension registries", e); + } + + final Map recommendedExtensionMap = new HashMap<>(latestCatalog.getExtensions().size()); + final Map> recommendedExtensionsByOrigin = new HashMap<>(); + for (Extension e : latestCatalog.getExtensions()) { + recommendedExtensionMap.put(e.getArtifact().getKey(), e); + for (ExtensionOrigin origin : e.getOrigins()) { + recommendedExtensionsByOrigin.computeIfAbsent(origin.getId(), k -> new ArrayList<>()) + .add(e.getArtifact().getKey()); + } + } + + final List notAvailableExtensions = new ArrayList<>(0); + final List recommendedExtensions = new ArrayList<>(previousExtensions.size()); + for (ArtifactKey key : previousExtensions.keySet()) { + final Extension e = recommendedExtensionMap.get(key); + if (e == null) { + notAvailableExtensions.add(e); + } else { + recommendedExtensions.add(e); + } + } + + if (!notAvailableExtensions.isEmpty()) { + final StringBuilder buf = new StringBuilder(); + buf.append( + "Could not find any information about the following extensions in the currently configured registries: "); + buf.append(notAvailableExtensions.get(0).getArtifact().getKey().toGacString()); + for (int i = 1; i < notAvailableExtensions.size(); ++i) { + buf.append(", ").append(notAvailableExtensions.get(i).getArtifact().getKey().toGacString()); + } + getLog().warn(buf.toString()); + return; + } + + final List recommendedOrigins; + try { + recommendedOrigins = getRecommendedOrigins(latestCatalog, recommendedExtensions); + } catch (QuarkusCommandException e) { + getLog().warn(e.getLocalizedMessage()); + return; + } + + final List previousBomImports = new ArrayList<>(); + for (Dependency d : project.getDependencyManagement().getDependencies()) { + if (PlatformArtifacts.isCatalogArtifactId(d.getArtifactId())) { + final ArtifactCoords platformBomCoords = new ArtifactCoords(d.getGroupId(), + PlatformArtifacts.ensureBomArtifactId(d.getArtifactId()), "pom", d.getVersion()); + if (d.getArtifactId().startsWith("quarkus-universe-bom-")) { + // in pre-2.x quarkus versions, the quarkus-bom descriptor would show up as a parent of the quarkus-universe-bom one + // even if it was not actually imported, so here we simply remove it, if it was found + previousBomImports.remove(new ArtifactCoords(platformBomCoords.getGroupId(), "quarkus-bom", "pom", + platformBomCoords.getVersion())); + } + previousBomImports.add(platformBomCoords); + } + } + + final List recommendedBomImports = new ArrayList<>(); + final Map nonPlatformUpdates = new LinkedHashMap<>(0); + + for (ExtensionCatalog origin : recommendedOrigins) { + if (origin.isPlatform()) { + if (!previousBomImports.remove(origin.getBom())) { + recommendedBomImports.add(origin.getBom()); + } + for (ArtifactKey extKey : recommendedExtensionsByOrigin.getOrDefault(origin.getId(), Collections.emptyList())) { + previousExtensions.remove(extKey); + } + } else { + for (ArtifactKey extKey : recommendedExtensionsByOrigin.getOrDefault(origin.getId(), Collections.emptyList())) { + final Extension recommendedExt = recommendedExtensionMap.get(extKey); + final String prevVersion = previousExtensions.remove(extKey); + if (prevVersion != null && !prevVersion.equals(recommendedExt.getArtifact().getVersion())) { + nonPlatformUpdates.put(recommendedExt.getArtifact(), prevVersion); + } + } + } + } + + if (recommendedBomImports.isEmpty() && nonPlatformUpdates.isEmpty()) { + log.info("The project is up-to-date"); + return; + } + + ArtifactCoords prevPluginCoords = null; + for (Plugin p : project.getBuildPlugins()) { + if (p.getArtifactId().equals("quarkus-maven-plugin")) { + prevPluginCoords = new ArtifactCoords(p.getGroupId(), p.getArtifactId(), p.getVersion()); + break; + } + } + + ExtensionCatalog core = null; + for (ExtensionOrigin o : recommendedOrigins) { + if (o.isPlatform() && o.getBom().getArtifactId().equals("quarkus-bom")) { + core = (ExtensionCatalog) o; + } + } + + ArtifactCoords recommendedPluginCoords = null; + if (core != null) { + final Map props = (Map) ((Map) core.getMetadata().getOrDefault("project", + Collections.emptyMap())).getOrDefault("properties", Collections.emptyMap()); + final String pluginGroupId = (String) props.get("maven-plugin-groupId"); + final String pluginArtifactId = (String) props.get("maven-plugin-artifactId"); + final String pluginVersion = (String) props.get("maven-plugin-version"); + if (pluginGroupId == null || pluginArtifactId == null || pluginVersion == null) { + log.warn("Failed to locate the recommended Quarkus Maven plugin coordinates"); + } else { + recommendedPluginCoords = new ArtifactCoords(pluginGroupId, pluginArtifactId, pluginVersion); + } + } + + final StringWriter buf = new StringWriter(); + try (BufferedWriter writer = new BufferedWriter(buf)) { + + writer.append("Currently recommended updates for the application include:"); + writer.newLine(); + writer.newLine(); + + if (!previousBomImports.isEmpty()) { + writer.append(" * BOM imports to be replaced:"); + writer.newLine(); + writer.newLine(); + for (ArtifactCoords bom : previousBomImports) { + logBomImport(writer, bom); + } + writer.newLine(); + } + + if (!recommendedBomImports.isEmpty()) { + writer.append(" * New recommended BOM imports:"); + writer.newLine(); + writer.newLine(); + for (ArtifactCoords bom : recommendedBomImports) { + logBomImport(writer, bom); + } + writer.newLine(); + } + + if (!nonPlatformUpdates.isEmpty()) { + writer.append(" * New recommended extension versions (not managed by the BOMs):"); + writer.newLine(); + writer.newLine(); + + for (ArtifactCoords coords : nonPlatformUpdates.keySet()) { + writer.append(" "); + writer.newLine(); + writer.append(" ").append(coords.getGroupId()).append(""); + writer.newLine(); + writer.append(" ").append(coords.getArtifactId()).append(""); + writer.newLine(); + writer.append(" ").append(coords.getVersion()).append(""); + writer.newLine(); + writer.append(" "); + writer.newLine(); + } + writer.newLine(); + } + + if (prevPluginCoords != null && recommendedPluginCoords != null + && !prevPluginCoords.equals(recommendedPluginCoords)) { + writer.append(" * Recommended Quarkus Maven plugin:"); + writer.newLine(); + writer.newLine(); + writer.append(" "); + writer.newLine(); + writer.append(" ").append(recommendedPluginCoords.getGroupId()).append(""); + writer.newLine(); + writer.append(" ").append(recommendedPluginCoords.getArtifactId()).append(""); + writer.newLine(); + writer.append(" ").append(recommendedPluginCoords.getVersion()).append(""); + writer.newLine(); + writer.append(" "); + writer.newLine(); + + } + } catch (IOException e) { + throw new MojoExecutionException("Failed to compose the update report", e); + } + + getLog().info(buf.toString()); + } + + private void logBomImport(BufferedWriter writer, ArtifactCoords bom) throws IOException { + writer.append(" "); + writer.newLine(); + writer.append(" ").append(bom.getGroupId()).append(""); + writer.newLine(); + writer.append(" ").append(bom.getArtifactId()).append(""); + writer.newLine(); + writer.append(" ").append(bom.getVersion()).append(""); + writer.newLine(); + writer.append(" pom"); + writer.newLine(); + writer.append(" import"); + writer.newLine(); + writer.append(" "); + writer.newLine(); + } + + private Map getDirectExtensionDependencies() throws MojoExecutionException { + final List modelDeps = project.getModel().getDependencies(); + final List requests = new ArrayList<>(modelDeps.size()); + for (Dependency d : modelDeps) { + if ("jar".equals(d.getType())) { + requests.add(new ArtifactRequest().setArtifact( + new DefaultArtifact(d.getGroupId(), d.getArtifactId(), d.getClassifier(), d.getType(), d.getVersion())) + .setRepositories(repos)); + } + } + final List artifactResults; + try { + artifactResults = repoSystem.resolveArtifacts(repoSession, requests); + } catch (ArtifactResolutionException e) { + throw new MojoExecutionException("Failed to resolve project dependencies", e); + } + final Map extensions = new HashMap<>(artifactResults.size()); + for (ArtifactResult ar : artifactResults) { + final Artifact a = ar.getArtifact(); + if (isExtension(a.getFile().toPath())) { + extensions.put(new ArtifactKey(a.getGroupId(), a.getArtifactId(), a.getClassifier(), a.getExtension()), + a.getVersion()); + } + } + return extensions; + } + + private static boolean isExtension(Path p) throws MojoExecutionException { + if (!Files.exists(p)) { + throw new MojoExecutionException("Extension artifact " + p + " does not exist"); + } + if (Files.isDirectory(p)) { + return Files.exists(p.resolve(BootstrapConstants.DESCRIPTOR_PATH)); + } else { + try (FileSystem fs = FileSystems.newFileSystem(p, (ClassLoader) null)) { + return Files.exists(fs.getPath(BootstrapConstants.DESCRIPTOR_PATH)); + } catch (IOException e) { + throw new MojoExecutionException("Failed to read archive " + p, e); + } + } + } + + private static List getRecommendedOrigins(ExtensionCatalog extensionCatalog, List extensions) + throws QuarkusCommandException { + final List extOrigins = new ArrayList<>(extensions.size()); + for (Extension e : extensions) { + addOrigins(extOrigins, e); + } + final OriginSelector os = new OriginSelector(extOrigins); + os.calculateCompatibleCombinations(); + + final OriginCombination recommendedCombination = os.getRecommendedCombination(); + if (recommendedCombination == null) { + final StringBuilder buf = new StringBuilder(); + buf.append("Failed to determine a compatible Quarkus version for the requested extensions: "); + buf.append(extensions.get(0).getArtifact().getKey().toGacString()); + for (int i = 1; i < extensions.size(); ++i) { + buf.append(", ").append(extensions.get(i).getArtifact().getKey().toGacString()); + } + throw new QuarkusCommandException(buf.toString()); + } + return recommendedCombination.getUniqueSortedOrigins().stream().map(o -> o.getCatalog()).collect(Collectors.toList()); + } + + private static void addOrigins(final List extOrigins, Extension e) { + ExtensionOrigins.Builder eoBuilder = null; + for (ExtensionOrigin o : e.getOrigins()) { + if (!(o instanceof ExtensionCatalog)) { + continue; + } + final ExtensionCatalog c = (ExtensionCatalog) o; + final OriginPreference op = (OriginPreference) c.getMetadata().get("origin-preference"); + if (op == null) { + continue; + } + if (eoBuilder == null) { + eoBuilder = ExtensionOrigins.builder(e.getArtifact().getKey()); + } + eoBuilder.addOrigin(c, op); + } + if (eoBuilder != null) { + extOrigins.add(eoBuilder.build()); + } + } +} diff --git a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java index e5e6a77cd8117..12df08a031baa 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java @@ -251,7 +251,7 @@ public void execute() throws MojoExecutionException { } } else if (containsAtLeastOneGradleFile) { throw new MojoExecutionException( - "You are trying to create maven project in a directory that contains only gradle build files."); + "You are trying to create a Maven project in a directory that contains only Gradle build files."); } } else if (BuildTool.GRADLE.equals(buildToolEnum) || BuildTool.GRADLE_KOTLIN_DSL.equals(buildToolEnum)) { if (containsAtLeastOneGradleFile) { diff --git a/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/grpc-codestart/base/src/main/resources/META-INF/resources/index.entry.qute.html b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/grpc-codestart/base/src/main/resources/META-INF/resources/index.entry.qute.html new file mode 100644 index 0000000000000..0317faba62263 --- /dev/null +++ b/devtools/project-core-extension-codestarts/src/main/resources/codestarts/quarkus/extension-codestarts/grpc-codestart/base/src/main/resources/META-INF/resources/index.entry.qute.html @@ -0,0 +1,3 @@ +{#include index-entry} +{#body}This codestart implements a simple gRPC service that can be tested in the Dev UI (available in dev mode only). +{/include} \ No newline at end of file diff --git a/docs/src/main/asciidoc/cli-tooling.adoc b/docs/src/main/asciidoc/cli-tooling.adoc index e2a39ca359959..c6ef10ea64b00 100644 --- a/docs/src/main/asciidoc/cli-tooling.adoc +++ b/docs/src/main/asciidoc/cli-tooling.adoc @@ -173,12 +173,12 @@ Both `quarkus create` and `quarkus extension list` allow you to explicitly speci 1. Specify a specific Platform Release BOM + -A https://quarkus.io/guides/platform#quarkus-platform-bom[Quarkus Platform release BOM] is identified by `groupId:artifactId:version` (GAV) coordinates. When specifying a platform release BOM, you may use empty segments to fallback to default values (groupId: `io.quarkus`, artifactId: `quarkus-bom`, version: cli version). If you specify only one segment (no `:`), it is assumed to be a version. +A https://quarkus.io/guides/platform#quarkus-platform-bom[Quarkus Platform release BOM] is identified by `groupId:artifactId:version` (GAV) coordinates. When specifying a platform release BOM, you may use empty segments to fallback to default values (shown with `quarkus create app --help`). If you specify only one segment (no `:`), it is assumed to be a version. + For example: + -- Given the `2.0.0.Final` version of the CLI, specifying `-P :quarkus-bom:` is equivalent to `-P io.quarkus.platform:quarkus-bom:2.0.0.Final`. -- Specifying `-P 999-SNAPSHOT` is equivalent to `-P io.quarkus:quarkus-bom:999-SNAPSHOT` +- With the `2.0.0.Final` version of the CLI, specifying `-P :quarkus-bom:` is equivalent to `-P io.quarkus:quarkus-bom:2.0.0.Final`. Specifying `-P 999-SNAPSHOT` is equivalent to `-P io.quarkus:quarkus-bom:999-SNAPSHOT`. +- With the `2.1.0.Final` version of the CLI, `io.quarkus.platform` is the default group id. Specifying `-P :quarkus-bom:` is equivalent to `-P io.quarkus.platform:quarkus-bom:2.1.0.Final`. Note that you need to specify the group id to work with a snapshot, e.g `-P io.quarkus::999-SNAPSHOT` is equivalent to `-P io.quarkus:quarkus-bom:999-SNAPSHOT`. + Note: default values are subject to change. Using the `--dry-run` option will show you the computed value. diff --git a/docs/src/main/asciidoc/images/dev-ui-oidc-card.png b/docs/src/main/asciidoc/images/dev-ui-oidc-card.png index 5bd40cab30bc6..b972a88330a7b 100644 Binary files a/docs/src/main/asciidoc/images/dev-ui-oidc-card.png and b/docs/src/main/asciidoc/images/dev-ui-oidc-card.png differ diff --git a/docs/src/main/asciidoc/images/dev-ui-oidc-keycloak-card.png b/docs/src/main/asciidoc/images/dev-ui-oidc-keycloak-card.png index e249301b3cb02..f88c3bbf8b027 100644 Binary files a/docs/src/main/asciidoc/images/dev-ui-oidc-keycloak-card.png and b/docs/src/main/asciidoc/images/dev-ui-oidc-keycloak-card.png differ diff --git a/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc b/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc index ee4109b2640bf..e31ad1b5aa98e 100644 --- a/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc +++ b/docs/src/main/asciidoc/kafka-schema-registry-avro.adoc @@ -45,7 +45,7 @@ However, you can go right to the completed example. Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. -The solution is located in the `kafka-avro-schema-quickstart` {quickstarts-tree-url}/kafka-quickstart-avro-schema[directory]. +The solution is located in the `kafka-avro-schema-quickstart` {quickstarts-tree-url}/kafka-avro-schema-quickstart[directory]. == Creating the Maven Project diff --git a/docs/src/main/asciidoc/mailer-reference.adoc b/docs/src/main/asciidoc/mailer-reference.adoc index d6e9af7836621..544b0a771c6e3 100644 --- a/docs/src/main/asciidoc/mailer-reference.adoc +++ b/docs/src/main/asciidoc/mailer-reference.adoc @@ -288,6 +288,11 @@ You can also create your own instance, and pass your own configuration. If you want to use the Gmail SMTP server, first create a dedicated password in `Google Account > Security > App passwords` or go to https://myaccount.google.com/apppasswords. +[NOTE] +==== +You need to switch on 2-Step Verification at https://myaccount.google.com/security in order to access the App passwords page. +==== + When done, you can configure your Quarkus application by adding the following properties to your `application.properties`: With TLS: diff --git a/docs/src/main/asciidoc/mailer.adoc b/docs/src/main/asciidoc/mailer.adoc index 2bde3fce3ea53..d61e20586f862 100644 --- a/docs/src/main/asciidoc/mailer.adoc +++ b/docs/src/main/asciidoc/mailer.adoc @@ -184,7 +184,7 @@ The Quarkus mailer is using SMTP, so make sure you have access to a SMTP server. In the `src/main/resources/application.properties` file, you need to configure the host, port, username, password as well as the other configuration aspect. Note that the password can also be configured using system properties and environment variables. -See the xref:configuration-reference.adoc[configuration reference guide] for details. +See the xref:config-reference.adoc[configuration reference guide] for details. Here is an example using _sendgrid_: diff --git a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc index 0fa38d53ae5df..614f14aa451af 100644 --- a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc @@ -318,7 +318,9 @@ This interface allows you to dynamically create tenant configurations at runtime package io.quarkus.it.keycloak; import javax.enterprise.context.ApplicationScoped; +import java.util.function.Supplier; +import io.smallrye.mutiny.Uni; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.TenantConfigResolver; import io.vertx.ext.web.RoutingContext; @@ -327,7 +329,7 @@ import io.vertx.ext.web.RoutingContext; public class CustomTenantConfigResolver implements TenantConfigResolver { @Override - public OidcTenantConfig resolve(RoutingContext context) { + public Uni resolve(RoutingContext context, TenantConfigResolver.TenantConfigRequestContext requestContext) { String path = context.request().path(); String[] parts = path.split("/"); @@ -337,24 +339,30 @@ public class CustomTenantConfigResolver implements TenantConfigResolver { } if ("tenant-c".equals(parts[1])) { - OidcTenantConfig config = new OidcTenantConfig(); + // Do 'return requestContext.runBlocking(createTenantConfig());' + // if a blocking call is required to create a tenant config + return Uni.createFromItem(createTenantConfig()); + } + + // resolve to default tenant configuration + return null; + } - config.setTenantId("tenant-c"); - config.setAuthServerUrl("http://localhost:8180/auth/realms/tenant-c"); - config.setClientId("multi-tenant-client"); - OidcTenantConfig.Credentials credentials = new OidcTenantConfig.Credentials(); + private Supplier createTenantConfig() { + final OidcTenantConfig config = new OidcTenantConfig(); - credentials.setSecret("my-secret"); + config.setTenantId("tenant-c"); + config.setAuthServerUrl("http://localhost:8180/auth/realms/tenant-c"); + config.setClientId("multi-tenant-client"); + OidcTenantConfig.Credentials credentials = new OidcTenantConfig.Credentials(); - config.setCredentials(credentials); + credentials.setSecret("my-secret"); - // any other setting support by the quarkus-oidc extension + config.setCredentials(credentials); - return config; - } + // any other setting support by the quarkus-oidc extension - // resolve to default tenant configuration - return null; + return () -> config; } } ---- diff --git a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc index 0a6f47b01151c..62671a68ace68 100644 --- a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc @@ -469,6 +469,59 @@ In such cases, you can use `quarkus.oidc.token-state-manager.split-tokens=true` Register your own `io.quarkus.oidc.TokenStateManager' implementation as an `@ApplicationScoped` CDI bean if you need to customize the way the tokens are associated with the session cookie. For example, you may want to keep the tokens in a database and have only a database pointer stored in a session cookie. Note though that it may present some challenges in making the tokens available across multiple microservices nodes. +Here is a simple example: + +[source, java] +---- +package io.quarkus.oidc.test; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import io.quarkus.arc.AlternativePriority; +import io.quarkus.oidc.AuthorizationCodeTokens; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TokenStateManager; +import io.quarkus.oidc.runtime.DefaultTokenStateManager; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +@AlternativePriority(1) +public class CustomTokenStateManager implements TokenStateManager { + + @Inject + DefaultTokenStateManager tokenStateManager; + + @Override + public Uni createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, + AuthorizationCodeTokens sessionContent, TokenStateManager.CreateTokenStateRequestContext requestContext) { + return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent, requestContext) + .map(t -> (t + "|custom")); + } + + @Override + public Uni getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, + String tokenState, TokenStateManager.GetTokensRequestContext requestContext) { + if (!tokenState.endsWith("|custom")) { + throw new IllegalStateException(); + } + String defaultState = tokenState.substring(0, tokenState.length() - 7); + return tokenStateManager.getTokens(routingContext, oidcConfig, defaultState, requestContext); + } + + @Override + public Uni deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, + TokenStateManager.DeleteTokensRequestContext requestContext) { + if (!tokenState.endsWith("|custom")) { + throw new IllegalStateException(); + } + String defaultState = tokenState.substring(0, tokenState.length() - 7); + return tokenStateManager.deleteTokens(routingContext, oidcConfig, defaultState, requestContext); + } +} +---- + == Listening to important authentication events One can register `@ApplicationScoped` bean which will observe important OIDC authentication events. The listener will be updated when a user has logged in for the first time or re-authenticated, as well as when the session has been refreshed. More events may be reported in the future. For example: diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceJdbcRuntimeConfig.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceJdbcRuntimeConfig.java index d586ddb8bb388..0d4b4c50470a5 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceJdbcRuntimeConfig.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceJdbcRuntimeConfig.java @@ -47,6 +47,12 @@ public class DataSourceJdbcRuntimeConfig { @ConfigItem(defaultValue = "2M") public Optional backgroundValidationInterval = Optional.of(Duration.ofMinutes(2)); + /** + * Perform foreground validation on connections that have been idle for longer than the specified interval. + */ + @ConfigItem + public Optional foregroundValidationInterval = Optional.empty(); + /** * The timeout before cancelling the acquisition of a new connection */ diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java index e5a1d8c5e9e31..fa6c4290da30e 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java @@ -276,6 +276,9 @@ private void applyNewConfiguration(AgroalDataSourceConfigurationSupplier dataSou if (dataSourceJdbcRuntimeConfig.backgroundValidationInterval.isPresent()) { poolConfiguration.validationTimeout(dataSourceJdbcRuntimeConfig.backgroundValidationInterval.get()); } + if (dataSourceJdbcRuntimeConfig.foregroundValidationInterval.isPresent()) { + poolConfiguration.idleValidationTimeout(dataSourceJdbcRuntimeConfig.foregroundValidationInterval.get()); + } if (dataSourceJdbcRuntimeConfig.validationQuerySql.isPresent()) { String validationQuery = dataSourceJdbcRuntimeConfig.validationQuerySql.get(); poolConfiguration.connectionValidator(new ConnectionValidator() { diff --git a/extensions/amazon-alexa/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-alexa/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 94df9bef06f78..609bf3df1dd9c 100644 --- a/extensions/amazon-alexa/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-alexa/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,4 +8,4 @@ metadata: categories: - "cloud" guide: "https://quarkus.io/guides/amazon-lambda" - status: "preview" + status: "stable" diff --git a/extensions/amazon-lambda-http/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-lambda-http/runtime/src/main/resources/META-INF/quarkus-extension.yaml index d26b340b6991f..295b2b80f286c 100644 --- a/extensions/amazon-lambda-http/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-lambda-http/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -11,4 +11,4 @@ metadata: categories: - "cloud" guide: "https://quarkus.io/guides/amazon-lambda-http" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/graal/LambdaContainerHandlerSubstitution.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/graal/LambdaContainerHandlerSubstitution.java index e2f2ede57953a..d1c9ffebdd196 100644 --- a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/graal/LambdaContainerHandlerSubstitution.java +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/graal/LambdaContainerHandlerSubstitution.java @@ -5,7 +5,7 @@ import com.oracle.svm.core.annotate.TargetClass; @TargetClass(LambdaContainerHandler.class) -public class LambdaContainerHandlerSubstitution { +public final class LambdaContainerHandlerSubstitution { // afterburner does not work in native mode, so let's ensure it's never registered @Substitute diff --git a/extensions/amazon-lambda-rest/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-lambda-rest/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 8caf7277ca0b6..3a755c8ed49a0 100644 --- a/extensions/amazon-lambda-rest/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-lambda-rest/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -11,4 +11,4 @@ metadata: categories: - "cloud" guide: "https://quarkus.io/guides/amazon-lambda-http" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/amazon-lambda-xray/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-lambda-xray/runtime/src/main/resources/META-INF/quarkus-extension.yaml index b68cb8deb0e6a..64721449014f4 100644 --- a/extensions/amazon-lambda-xray/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-lambda-xray/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -10,4 +10,4 @@ metadata: guide: "https://quarkus.io/guides/amazon-lambda#tracing-with-aws-xray-and-graalvm" categories: - "cloud" - status: "preview" \ No newline at end of file + status: "stable" \ No newline at end of file diff --git a/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 4e66c6d21a76a..2c4356bfb26d4 100644 --- a/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,7 +8,7 @@ metadata: categories: - "cloud" guide: "https://quarkus.io/guides/amazon-lambda" - status: "preview" + status: "stable" codestart: name: "amazon-lambda" kind: "example" diff --git a/extensions/amazon-services/common/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/common/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 4ea5a1d78f630..86c58a06796c5 100644 --- a/extensions/amazon-services/common/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/common/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,5 +8,5 @@ metadata: - "amazon" categories: - "data" - status: "preview" + status: "stable" unlisted: true \ No newline at end of file diff --git a/extensions/amazon-services/dynamodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/dynamodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 16a93626af4f8..17da2de4d91bc 100644 --- a/extensions/amazon-services/dynamodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/dynamodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -10,4 +10,4 @@ metadata: guide: "https://quarkus.io/guides/amazon-dynamodb" categories: - "data" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/amazon-services/iam/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/iam/runtime/src/main/resources/META-INF/quarkus-extension.yaml index b23a03f1897e5..b7c67762ea2f0 100644 --- a/extensions/amazon-services/iam/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/iam/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,4 +9,4 @@ metadata: guide: "https://quarkus.io/guides/amazon-iam" categories: - "data" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/amazon-services/kms/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/kms/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 37f5135cba20b..64694f803b24d 100644 --- a/extensions/amazon-services/kms/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/kms/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,4 +9,4 @@ metadata: guide: "https://quarkus.io/guides/amazon-kms" categories: - "data" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/amazon-services/s3/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/s3/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 837a6866e3f99..cf0a788472a63 100644 --- a/extensions/amazon-services/s3/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/s3/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,4 +9,4 @@ metadata: guide: "https://quarkus.io/guides/amazon-s3" categories: - "data" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/amazon-services/ses/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/ses/runtime/src/main/resources/META-INF/quarkus-extension.yaml index ea0cb367cd76e..5eb9296cd7521 100644 --- a/extensions/amazon-services/ses/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/ses/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,4 +9,4 @@ metadata: guide: "https://quarkus.io/guides/amazon-ses" categories: - "data" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/amazon-services/sns/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/sns/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 507c0dc7dd321..62842e0b8d177 100644 --- a/extensions/amazon-services/sns/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/sns/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,4 +9,4 @@ metadata: guide: "https://quarkus.io/guides/amazon-sns" categories: - "data" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/amazon-services/sqs/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/sqs/runtime/src/main/resources/META-INF/quarkus-extension.yaml index c033c7456bb2d..a2f8ad1d26be9 100644 --- a/extensions/amazon-services/sqs/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/sqs/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,4 +9,4 @@ metadata: guide: "https://quarkus.io/guides/amazon-sqs" categories: - "data" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/amazon-services/ssm/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/ssm/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 8e077230c651f..77b04c7bebcb6 100644 --- a/extensions/amazon-services/ssm/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/ssm/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,4 +9,4 @@ metadata: guide: "https://quarkus.io/guides/amazon-ssm" categories: - "data" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchivePredicateBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchivePredicateBuildItem.java new file mode 100644 index 0000000000000..26aabbf179746 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchivePredicateBuildItem.java @@ -0,0 +1,25 @@ +package io.quarkus.arc.deployment; + +import java.util.function.Predicate; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.ApplicationArchive; + +/** + * + * By default, only explict/implicit bean archives (as defined by the spec) are considered during the bean discovery. However, + * extensions can register a logic to identify additional bean archives. + */ +public final class BeanArchivePredicateBuildItem extends MultiBuildItem { + + private final Predicate predicate; + + public BeanArchivePredicateBuildItem(Predicate predicate) { + this.predicate = predicate; + } + + public Predicate getPredicate() { + return predicate; + } + +} diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java index 2bb662fd5a7b0..8133de96db552 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java @@ -1,6 +1,11 @@ package io.quarkus.arc.deployment; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import org.jboss.jandex.AnnotationInstance; @@ -35,12 +40,13 @@ public BeanArchiveIndexBuildItem build(ArcConfig config, ApplicationArchivesBuil List additionalBeanDefiningAnnotations, List additionalBeans, List generatedBeans, LiveReloadBuildItem liveReloadBuildItem, BuildProducer generatedClass, - CustomScopeAnnotationsBuildItem customScopes, List excludeDependencyBuildItems) + CustomScopeAnnotationsBuildItem customScopes, List excludeDependencyBuildItems, + List beanArchivePredicates) throws Exception { // First build an index from application archives IndexView applicationIndex = buildApplicationIndex(config, applicationArchivesBuildItem, - additionalBeanDefiningAnnotations, customScopes, excludeDependencyBuildItems); + additionalBeanDefiningAnnotations, customScopes, excludeDependencyBuildItems, beanArchivePredicates); // Then build additional index for beans added by extensions Indexer additionalBeanIndexer = new Indexer(); @@ -81,7 +87,8 @@ public BeanArchiveIndexBuildItem build(ArcConfig config, ApplicationArchivesBuil private IndexView buildApplicationIndex(ArcConfig config, ApplicationArchivesBuildItem applicationArchivesBuildItem, List additionalBeanDefiningAnnotations, - CustomScopeAnnotationsBuildItem customScopes, List excludeDependencyBuildItems) { + CustomScopeAnnotationsBuildItem customScopes, List excludeDependencyBuildItems, + List beanArchivePredicates) { Set archives = applicationArchivesBuildItem.getAllApplicationArchives(); @@ -116,10 +123,8 @@ private IndexView buildApplicationIndex(ArcConfig config, ApplicationArchivesBui continue; } IndexView index = archive.getIndex(); - // NOTE: Implicit bean archive without beans.xml contains one or more bean classes with a bean defining annotation and no extension - if (archive.getChildPath("META-INF/beans.xml") != null || archive.getChildPath("WEB-INF/beans.xml") != null - || (index.getAllKnownImplementors(DotNames.EXTENSION).isEmpty() - && containsBeanDefiningAnnotation(index, beanDefiningAnnotations))) { + if (isExplicitBeanArchive(archive) || isImplicitBeanArchive(index, beanDefiningAnnotations) + || isAdditionalBeanArchive(archive, beanArchivePredicates)) { indexes.add(index); } } @@ -127,6 +132,26 @@ && containsBeanDefiningAnnotation(index, beanDefiningAnnotations))) { return CompositeIndex.create(indexes); } + private boolean isExplicitBeanArchive(ApplicationArchive archive) { + return archive.getChildPath("META-INF/beans.xml") != null || archive.getChildPath("WEB-INF/beans.xml") != null; + } + + private boolean isImplicitBeanArchive(IndexView index, Set beanDefiningAnnotations) { + // NOTE: Implicit bean archive without beans.xml contains one or more bean classes with a bean defining annotation and no extension + return index.getAllKnownImplementors(DotNames.EXTENSION).isEmpty() + && containsBeanDefiningAnnotation(index, beanDefiningAnnotations); + } + + private boolean isAdditionalBeanArchive(ApplicationArchive archive, + List beanArchivePredicates) { + for (BeanArchivePredicateBuildItem p : beanArchivePredicates) { + if (p.getPredicate().test(archive)) { + return true; + } + } + return false; + } + private boolean isApplicationArchiveExcluded(ArcConfig config, List excludeDependencyBuildItems, ApplicationArchive archive) { if (archive.getArtifactKey() != null) { diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java index ee60809ab577f..a0d855f1ce38f 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java @@ -1,12 +1,17 @@ package io.quarkus.arc.deployment; +import static io.quarkus.arc.deployment.ConfigClassBuildItem.Type.MAPPING; +import static io.quarkus.arc.deployment.ConfigClassBuildItem.Type.PROPERTIES; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; -import static io.quarkus.deployment.configuration.ConfigMappingUtils.CONFIG_MAPPING_NAME; import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; +import static java.util.Collections.emptySet; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; +import static org.eclipse.microprofile.config.inject.ConfigProperties.UNCONFIGURED_PREFIX; import static org.jboss.jandex.AnnotationInstance.create; import static org.jboss.jandex.AnnotationTarget.Kind.CLASS; +import static org.jboss.jandex.AnnotationTarget.Kind.FIELD; +import static org.jboss.jandex.AnnotationTarget.Kind.METHOD_PARAMETER; import static org.jboss.jandex.AnnotationValue.createStringValue; import java.util.ArrayList; @@ -14,6 +19,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.Supplier; import java.util.stream.Stream; @@ -25,8 +31,10 @@ import org.eclipse.microprofile.config.inject.ConfigProperties; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; @@ -46,15 +54,16 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.ConfigClassBuildItem; import io.quarkus.deployment.builditem.ConfigurationBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.deployment.configuration.ConfigMappingUtils; import io.quarkus.deployment.configuration.definition.RootDefinition; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.gizmo.ResultHandle; import io.quarkus.runtime.annotations.ConfigPhase; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.ConfigMappingLoader; +import io.smallrye.config.ConfigMappingMetadata; import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; import io.smallrye.config.inject.ConfigProducer; @@ -66,6 +75,7 @@ public class ConfigBuildStep { private static final DotName MP_CONFIG_PROPERTIES_NAME = DotName.createSimple(ConfigProperties.class.getName()); private static final DotName MP_CONFIG_VALUE_NAME = DotName.createSimple(ConfigValue.class.getName()); + private static final DotName CONFIG_MAPPING_NAME = DotName.createSimple(ConfigMapping.class.getName()); private static final DotName MAP_NAME = DotName.createSimple(Map.class.getName()); private static final DotName SET_NAME = DotName.createSimple(Set.class.getName()); private static final DotName LIST_NAME = DotName.createSimple(List.class.getName()); @@ -242,11 +252,79 @@ void generateConfigClasses( BuildProducer reflectiveClasses, BuildProducer configClasses) { - // TODO - Generation of Mapping interface classes can be done in core because they don't require CDI - ConfigMappingUtils.generateConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses, - CONFIG_MAPPING_NAME); - ConfigMappingUtils.generateConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses, - MP_CONFIG_PROPERTIES_NAME); + List mappingAnnotations = new ArrayList<>(); + mappingAnnotations.addAll(combinedIndex.getIndex().getAnnotations(CONFIG_MAPPING_NAME)); + mappingAnnotations.addAll(combinedIndex.getIndex().getAnnotations(MP_CONFIG_PROPERTIES_NAME)); + + for (AnnotationInstance instance : mappingAnnotations) { + AnnotationTarget target = instance.target(); + AnnotationValue annotationPrefix = instance.value("prefix"); + + if (target.kind().equals(FIELD)) { + if (annotationPrefix != null && !annotationPrefix.asString().equals(UNCONFIGURED_PREFIX)) { + configClasses.produce( + toConfigClassBuildItem(instance, toClass(target.asField().type().name()), + annotationPrefix.asString())); + continue; + } + } + + if (target.kind().equals(METHOD_PARAMETER)) { + if (annotationPrefix != null && !annotationPrefix.asString().equals(UNCONFIGURED_PREFIX)) { + ClassType classType = target.asMethodParameter().method().parameters() + .get(target.asMethodParameter().position()).asClassType(); + configClasses + .produce(toConfigClassBuildItem(instance, toClass(classType.name()), annotationPrefix.asString())); + continue; + } + } + + if (!target.kind().equals(CLASS)) { + continue; + } + + Class configClass = toClass(target.asClass().name()); + String prefix = Optional.ofNullable(annotationPrefix).map(AnnotationValue::asString).orElse(""); + + List configMappingsMetadata = ConfigMappingLoader.getConfigMappingsMetadata(configClass); + Set generatedClassesNames = new HashSet<>(); + Set mappingsInfo = new HashSet<>(); + configMappingsMetadata.forEach(mappingMetadata -> { + generatedClasses.produce( + new GeneratedClassBuildItem(true, mappingMetadata.getClassName(), mappingMetadata.getClassBytes())); + reflectiveClasses + .produce(ReflectiveClassBuildItem.builder(mappingMetadata.getInterfaceType()).methods(true).build()); + reflectiveClasses + .produce(ReflectiveClassBuildItem.builder(mappingMetadata.getClassName()).constructors(true).build()); + + for (Class parent : getHierarchy(mappingMetadata.getInterfaceType())) { + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(parent).methods(true).build()); + } + + generatedClassesNames.add(mappingMetadata.getClassName()); + + ClassInfo mappingInfo = combinedIndex.getIndex() + .getClassByName(DotName.createSimple(mappingMetadata.getInterfaceType().getName())); + if (mappingInfo != null) { + mappingsInfo.add(mappingInfo); + } + }); + + // Search and register possible classes for implicit Converter methods + for (ClassInfo classInfo : mappingsInfo) { + for (MethodInfo method : classInfo.methods()) { + if (!isHandledByProducers(method.returnType()) && + mappingsInfo.stream() + .map(ClassInfo::name) + .noneMatch(name -> name.equals(method.returnType().name()))) { + reflectiveClasses + .produce(new ReflectiveClassBuildItem(true, false, method.returnType().name().toString())); + } + } + } + + configClasses.produce(toConfigClassBuildItem(instance, configClass, generatedClassesNames, prefix)); + } } @BuildStep @@ -327,6 +405,12 @@ void registerConfigClasses( configClassWithPrefix -> Stream.of(configClassWithPrefix.getKlass(), configClassWithPrefix.getPrefix()) .collect(toList())); + recorder.registerConfigMappings( + configClasses.stream() + .filter(ConfigClassBuildItem::isMapping) + .map(configMapping -> configClassWithPrefix(configMapping.getConfigClass(), configMapping.getPrefix())) + .collect(toSet())); + recorder.registerConfigProperties( configClasses.stream() .filter(ConfigClassBuildItem::isProperties) @@ -335,6 +419,45 @@ void registerConfigClasses( .collect(toSet())); } + private static Class toClass(DotName dotName) { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + try { + return classLoader.loadClass(dotName.toString()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("The class (" + dotName.toString() + ") cannot be created during deployment.", e); + } + } + + private static ConfigClassBuildItem toConfigClassBuildItem( + AnnotationInstance instance, + Class configClass, + String prefix) { + return toConfigClassBuildItem(instance, configClass, emptySet(), prefix); + } + + private static ConfigClassBuildItem toConfigClassBuildItem( + AnnotationInstance instance, + Class configClass, + Set generatedClasses, + String prefix) { + if (instance.name().equals(CONFIG_MAPPING_NAME)) { + return new ConfigClassBuildItem(configClass, generatedClasses, prefix, MAPPING); + } else if (instance.name().equals(MP_CONFIG_PROPERTIES_NAME)) { + return new ConfigClassBuildItem(configClass, generatedClasses, prefix, PROPERTIES); + } else { + throw new IllegalArgumentException(); + } + } + + private static List> getHierarchy(Class mapping) { + List> interfaces = new ArrayList<>(); + for (Class i : mapping.getInterfaces()) { + interfaces.add(i); + interfaces.addAll(getHierarchy(i)); + } + return interfaces; + } + private String getPropertyName(String name, ClassInfo declaringClass) { StringBuilder builder = new StringBuilder(); if (declaringClass.enclosingClass() == null) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ConfigClassBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigClassBuildItem.java similarity index 94% rename from core/deployment/src/main/java/io/quarkus/deployment/builditem/ConfigClassBuildItem.java rename to extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigClassBuildItem.java index fb3e80e17225e..1d011ddbe5824 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ConfigClassBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigClassBuildItem.java @@ -1,4 +1,4 @@ -package io.quarkus.deployment.builditem; +package io.quarkus.arc.deployment; import java.util.Set; @@ -48,6 +48,6 @@ public boolean isProperties() { public enum Type { MAPPING, - PROPERTIES + PROPERTIES; } } diff --git a/extensions/container-image/container-image-openshift/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/container-image/container-image-openshift/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 5f6b78609781c..9c1ee15c7c8bb 100644 --- a/extensions/container-image/container-image-openshift/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/container-image/container-image-openshift/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,4 +8,4 @@ metadata: - "image" categories: - "cloud" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java index a87294e34fabb..d162a7dd8efb5 100644 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java @@ -28,12 +28,14 @@ import io.grpc.internal.ServerImpl; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; +import io.quarkus.arc.deployment.BeanArchivePredicateBuildItem; import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem; import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem; import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.BuiltinScope; +import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; @@ -218,7 +220,7 @@ private Set gatherBlockingMethods(ClassInfo service) { AnnotationsTransformerBuildItem transformUserDefinedServices(CombinedIndexBuildItem combinedIndexBuildItem, CustomScopeAnnotationsBuildItem customScopes) { // User-defined services usually only declare the @GrpcService qualifier - // We need to add @GrpcEnableRequestContext and @Singleton if needed + // We need to add @GrpcEnableRequestContext and @Singleton if needed Set userDefinedServices = new HashSet<>(); for (AnnotationInstance annotation : combinedIndexBuildItem.getIndex().getAnnotations(GrpcDotNames.GRPC_SERVICE)) { if (annotation.target().kind() == Kind.CLASS) { @@ -393,4 +395,16 @@ void configureMetrics(GrpcBuildTimeConfig configuration, Optional() { + + @Override + public boolean test(ApplicationArchive archive) { + // Every archive that contains a generated implementor of MutinyBean is considered a bean archive + return !archive.getIndex().getKnownDirectImplementors(GrpcDotNames.MUTINY_BEAN).isEmpty(); + } + }); + } + } diff --git a/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 5605cd44a5031..e1c04e3a8a33d 100644 --- a/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,7 +8,7 @@ metadata: - "web" - "serialization" - "reactive" - status: "experimental" + status: "stable" codestart: name: "grpc" languages: diff --git a/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java b/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java index b854dcada8f55..61048fcf548c7 100644 --- a/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java +++ b/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java @@ -49,6 +49,7 @@ import io.quarkus.arc.deployment.AutoAddScopeBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; +import io.quarkus.arc.deployment.ConfigClassBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.arc.processor.BeanInfo; @@ -62,7 +63,6 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.ConfigClassBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/jaxrs/ResteasyReactiveContextLocaleResolver.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/jaxrs/ResteasyReactiveContextLocaleResolver.java index 38366401d34e9..9f4023f718699 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/jaxrs/ResteasyReactiveContextLocaleResolver.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/jaxrs/ResteasyReactiveContextLocaleResolver.java @@ -18,6 +18,11 @@ public ResteasyReactiveContextLocaleResolver(HttpHeaders headers) { @Override protected HttpHeaders getHeaders() { - return headers; + try { + headers.getLength(); // this forces the creation of the actual object which will fail if there is no request in flight + return headers; + } catch (IllegalStateException e) { + return null; + } } } diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java new file mode 100644 index 0000000000000..3970870f72b53 --- /dev/null +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java @@ -0,0 +1,105 @@ +package io.quarkus.jdbc.oracle.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.ExcludeConfigBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageAllowIncompleteClasspathBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; + +/** + * The Oracle JDBC driver includes a {@literal META-INF/native-image} which enables a set + * of global flags we need to control better, so to ensure such flags do not interfere + * with requirements of other libraries. + *

+ * For this reason, the {@literal META-INF/native-image/native-image.properties} resource + * is excluded explicitly; then we re-implement the equivalent directives using Quarkus + * build items. + *

+ * Other resources such as {@literal jni-config.json} and {@literal resource-config.json} + * are not excluded, so to ensure we match the recommendations from the Oracle JDBC + * engineering team and make it easier to pick up improvements in these when the driver + * gets updated. + *

+ * Regarding {@literal reflect-config.json}, we also prefer excluding it for the time + * being even though it's strictly not necessary: the reason is that the previous driver + * version had a build-breaking mistake; this was fixed in version 21.3 so should no + * longer be necessary, but the previous driver had been tested more widely and would + * require it, so this would facilitate the option to revert to the older version in + * case of problems. + */ +public final class OracleMetadataOverrides { + + static final String DRIVER_JAR_MATCH_REGEX = ".*com\\.oracle\\.database\\.jdbc.*"; + static final String NATIVE_IMAGE_RESOURCE_MATCH_REGEX = "/META-INF/native-image/(?:native-image\\.properties|reflect-config\\.json)"; + + /** + * Should match the contents of {@literal reflect-config.json} + * + * @param reflectiveClass builItem producer + */ + @BuildStep + void build(BuildProducer reflectiveClass) { + //This is to match the Oracle metadata (which we excluded so that we can apply fixes): + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, false, "oracle.jdbc.internal.ACProxyable")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.jdbc.driver.T4CDriverExtension")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.jdbc.driver.T2CDriverExtension")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.jdbc.driver.ShardingDriverExtension")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.net.ano.Ano")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.net.ano.AuthenticationService")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.net.ano.DataIntegrityService")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.net.ano.EncryptionService")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.net.ano.SupervisorService")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.jdbc.driver.Message11")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, true, "oracle.sql.TypeDescriptor")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.sql.TypeDescriptorFactory")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.sql.AnyDataFactory")); + reflectiveClass + .produce(new ReflectiveClassBuildItem(true, false, false, "com.sun.rowset.providers.RIOptimisticProvider")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, false, "oracle.jdbc.logging.annotations.Supports")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, false, "oracle.jdbc.logging.annotations.Feature")); + } + + @BuildStep + void runtimeInitializeDriver(BuildProducer runtimeInitialized) { + //These re-implement all the "--initialize-at-build-time" arguments found in the native-image.properties : + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.OracleDriver")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.OracleDriver")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("java.sql.DriverManager")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.LogicalConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.pool.OraclePooledConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.pool.OracleDataSource")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.datasource.impl.OracleDataSource")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.pool.OracleOCIConnectionPool")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.OracleTimeoutThreadPerVM")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TimeoutInterruptHandler")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.HAManager")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.Clock")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TcpMultiplexer")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TcpMultiplexer")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TcpMultiplexer$LazyHolder")); + runtimeInitialized + .produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.BlockSource$ThreadedCachingBlockSource")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem( + "oracle.jdbc.driver.BlockSource$ThreadedCachingBlockSource$BlockReleaser")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.xa.client.OracleXADataSource")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.replay.OracleXADataSourceImpl")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.datasource.OracleXAConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.xa.client.OracleXAConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.xa.client.OracleXAHeteroConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.T4CXAConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.security.o5logon.O5Logon")); + } + + @BuildStep + ExcludeConfigBuildItem excludeOracleDirectives() { + // Excludes both native-image.properties and reflect-config.json, which are reimplemented above + return new ExcludeConfigBuildItem(DRIVER_JAR_MATCH_REGEX, NATIVE_IMAGE_RESOURCE_MATCH_REGEX); + } + + @BuildStep + NativeImageAllowIncompleteClasspathBuildItem naughtyDriver() { + return new NativeImageAllowIncompleteClasspathBuildItem("quarkus-jdbc-oracle"); + } + +} diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleReflections.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleReflections.java index 53854430001ff..118a24d4c1fdd 100644 --- a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleReflections.java +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleReflections.java @@ -3,7 +3,6 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; /** * Registers the {@code oracle.jdbc.driver.OracleDriver} so that it can be loaded @@ -24,22 +23,4 @@ void build(BuildProducer reflectiveClass) { reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, driverName)); } - @BuildStep - void runtimeInitializeDriver(BuildProducer runtimeInitialized) { - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.OracleDriver")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.SQLUtil$XMLFactory")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.NamedTypeAccessor$XMLFactory")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.OracleTimeoutThreadPerVM")); - runtimeInitialized - .produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.BlockSource$ThreadedCachingBlockSource")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.T4CTTIoauthenticate")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TcpMultiplexer$LazyHolder")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.security.o5logon.O5Logon")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem( - "oracle.jdbc.driver.BlockSource$ThreadedCachingBlockSource$BlockReleaser")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TimeoutInterruptHandler")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.Clock")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.NoSupportHAManager")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.LogicalConnection")); - } } diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/test/java/io/quarkus/jdbc/oracle/deployment/RegexMatchTest.java b/extensions/jdbc/jdbc-oracle/deployment/src/test/java/io/quarkus/jdbc/oracle/deployment/RegexMatchTest.java new file mode 100644 index 0000000000000..5a66b4d51b26a --- /dev/null +++ b/extensions/jdbc/jdbc-oracle/deployment/src/test/java/io/quarkus/jdbc/oracle/deployment/RegexMatchTest.java @@ -0,0 +1,45 @@ +package io.quarkus.jdbc.oracle.deployment; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; + +import io.smallrye.common.constraint.Assert; + +/** + * The metadata override facility of GraalVM's native-image + * works with regular expressions. + * We're testing our expressions here to match against the + * constants the compiler is expecting (inferred by debugging + * the compiler) as it's otherwise a bit tricky to assert + * if they have been applied. + */ +public class RegexMatchTest { + + @Test + public void jarRegexIsMatching() { + final String EXAMPLE_CLASSPATH = "/home/sanne/sources/quarkus/integration-tests/jpa-oracle/target/quarkus-integration-test-jpa-oracle-999-SNAPSHOT-native-image-source-jar/lib/com.oracle.database.jdbc.ojdbc11-21.3.0.0.jar"; + final Pattern pattern = Pattern.compile(OracleMetadataOverrides.DRIVER_JAR_MATCH_REGEX); + final Matcher matcher = pattern.matcher(EXAMPLE_CLASSPATH); + Assert.assertTrue(matcher.find()); + } + + @Test + public void resourceRegexIsMatching() { + //We need to exclude both of these: + final String RES1 = "/META-INF/native-image/native-image.properties"; + final String RES2 = "/META-INF/native-image/reflect-config.json"; + final Pattern pattern = Pattern.compile(OracleMetadataOverrides.NATIVE_IMAGE_RESOURCE_MATCH_REGEX); + + Assert.assertTrue(pattern.matcher(RES1).find()); + Assert.assertTrue(pattern.matcher(RES2).find()); + + //While this one should NOT be ignored: + final String RES3 = "/META-INF/native-image/resource-config.json"; + final String RES4 = "/META-INF/native-image/jni-config.json"; + Assert.assertFalse(pattern.matcher(RES3).find()); + Assert.assertFalse(pattern.matcher(RES4).find()); + } + +} diff --git a/extensions/jdbc/jdbc-oracle/runtime/pom.xml b/extensions/jdbc/jdbc-oracle/runtime/pom.xml index 9d56502238fd0..88044be2b5ec5 100644 --- a/extensions/jdbc/jdbc-oracle/runtime/pom.xml +++ b/extensions/jdbc/jdbc-oracle/runtime/pom.xml @@ -24,7 +24,7 @@ com.oracle.database.jdbc - ojdbc8 + ojdbc11 org.graalvm.nativeimage diff --git a/extensions/kubernetes-client/deployment-internal/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientErrorHandler.java b/extensions/kubernetes-client/deployment-internal/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientErrorHandler.java index ed127a722a83b..58f3eff0b634e 100644 --- a/extensions/kubernetes-client/deployment-internal/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientErrorHandler.java +++ b/extensions/kubernetes-client/deployment-internal/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientErrorHandler.java @@ -12,11 +12,11 @@ public static void handle(Exception e) { if (e.getCause() instanceof SSLHandshakeException) { LOG.error( "The application could not be deployed to the cluster because the Kubernetes API Server certificates are not trusted. The certificates can be configured using the relevant configuration properties under the 'quarkus.kubernetes-client' config root, or \"quarkus.kubernetes-client.trust-certs=true\" can be set to explicitly trust the certificates (not recommended)"); - if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } else { - throw new RuntimeException(e); - } + } + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new RuntimeException(e); } } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java index c358fb29ea50d..362b63ae06357 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java @@ -11,11 +11,12 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Path; -import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -25,6 +26,7 @@ import io.dekorate.utils.Serialization; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesList; +import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.openshift.api.model.Route; @@ -42,7 +44,6 @@ import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.kubernetes.client.deployment.KubernetesClientErrorHandler; import io.quarkus.kubernetes.client.spi.KubernetesClientBuildItem; -import io.quarkus.kubernetes.spi.KubernetesDeploymentTargetBuildItem; public class KubernetesDeployer { @@ -60,7 +61,7 @@ public void selectDeploymentTarget(ContainerImageInfoBuildItem containerImageInf Optional activeContainerImageCapability = ContainerImageCapabilitiesUtil .getActiveContainerImageCapability(capabilities); - if (!activeContainerImageCapability.isPresent()) { + if (activeContainerImageCapability.isEmpty()) { // we can't thrown an exception here, because it could prevent the Kubernetes resources from being generated return; } @@ -85,10 +86,10 @@ public void deploy(KubernetesClientBuildItem kubernetesClient, return; } - if (!selectedDeploymentTarget.isPresent()) { + if (selectedDeploymentTarget.isEmpty()) { - if (!ContainerImageCapabilitiesUtil - .getActiveContainerImageCapability(capabilities).isPresent()) { + if (ContainerImageCapabilitiesUtil + .getActiveContainerImageCapability(capabilities).isEmpty()) { throw new RuntimeException( "A Kubernetes deployment was requested but no extension was found to build a container image. Consider adding one of following extensions: " + CONTAINER_IMAGE_EXTENSIONS_STR + "."); @@ -158,11 +159,6 @@ private DeploymentTargetEntry determineDeploymentTarget( return selectedTarget; } - private Optional getOptionalDeploymentTarget( - List deploymentTargets, String name) { - return deploymentTargets.stream().filter(d -> name.equals(d.getName())).findFirst(); - } - private DeploymentResultBuildItem deploy(DeploymentTargetEntry deploymentTarget, KubernetesClient client, Path outputDir, OpenshiftConfig openshiftConfig, ApplicationInfoBuildItem applicationInfo) { @@ -174,12 +170,13 @@ private DeploymentResultBuildItem deploy(DeploymentTargetEntry deploymentTarget, try (FileInputStream fis = new FileInputStream(manifest)) { KubernetesList list = Serialization.unmarshalAsList(fis); - distinct(list.getItems()).forEach(i -> { - if (KNATIVE.equals(deploymentTarget.getName().toLowerCase())) { - client.resource(i).inNamespace(namespace).deletingExisting().createOrReplace(); - } else { - client.resource(i).inNamespace(namespace).createOrReplace(); + list.getItems().stream().filter(distinctByResourceKey()).forEach(i -> { + final var r = client.resource(i).inNamespace(namespace); + if (shouldDeleteExisting(deploymentTarget, i)) { + r.delete(); + r.waitUntilCondition(Objects::isNull, 10, TimeUnit.SECONDS); } + r.createOrReplace(); log.info("Applied: " + i.getKind() + " " + i.getMetadata().getName() + "."); }); @@ -221,13 +218,15 @@ private void printExposeInformation(KubernetesClient client, KubernetesList list } } - public static Predicate distictByResourceKey() { + private static boolean shouldDeleteExisting(DeploymentTargetEntry deploymentTarget, HasMetadata resource) { + return KNATIVE.equalsIgnoreCase(deploymentTarget.getName()) + || resource instanceof Service + || (Objects.equals("v1", resource.getApiVersion()) && Objects.equals("Service", resource.getKind())); + } + + private static Predicate distinctByResourceKey() { Map seen = new ConcurrentHashMap<>(); return t -> seen.putIfAbsent(t.getApiVersion() + "/" + t.getKind() + ":" + t.getMetadata().getName(), Boolean.TRUE) == null; } - - private static Collection distinct(Collection resources) { - return resources.stream().filter(distictByResourceKey()).collect(Collectors.toList()); - } } diff --git a/extensions/mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 658ebabc08bea..001d82655819a 100644 --- a/extensions/mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -10,4 +10,4 @@ metadata: - "Reactor" categories: - "reactive" - status: "preview" \ No newline at end of file + status: "stable" \ No newline at end of file diff --git a/extensions/oidc-client-filter/runtime/pom.xml b/extensions/oidc-client-filter/runtime/pom.xml index c590a6b40ca15..a942bd43fc823 100644 --- a/extensions/oidc-client-filter/runtime/pom.xml +++ b/extensions/oidc-client-filter/runtime/pom.xml @@ -12,7 +12,7 @@ quarkus-oidc-client-filter Quarkus - OpenID Connect Client Filter - Runtime - Use JAX-RS client filter to get and refresh the access tokens and set them as HTTP Authorization Bearer values + Use JAX-RS Client filter to get and refresh access tokens with OpenId Connect Client and send them as HTTP Authorization Bearer tokens io.quarkus diff --git a/extensions/oidc-client-filter/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/oidc-client-filter/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 48d29eeda4cee..0ff5af4249bd9 100644 --- a/extensions/oidc-client-filter/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/oidc-client-filter/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -11,4 +11,4 @@ metadata: guide: "https://quarkus.io/guides/security-openid-connect-client" categories: - "security" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/oidc-client-reactive-filter/runtime/pom.xml b/extensions/oidc-client-reactive-filter/runtime/pom.xml index 31034ca04b9a7..9fbb3bd9b2593 100644 --- a/extensions/oidc-client-reactive-filter/runtime/pom.xml +++ b/extensions/oidc-client-reactive-filter/runtime/pom.xml @@ -12,7 +12,7 @@ quarkus-oidc-client-reactive-filter Quarkus - OpenID Connect Client Reactive Filter - Runtime - Use Reactive RestClient filter to get and refresh the access tokens and set them as HTTP Authorization Bearer values + Use Reactive RestClient filter to get and refresh access tokens with OpenId Connect Client and send them as HTTP Authorization Bearer tokens io.quarkus diff --git a/extensions/oidc-client/runtime/pom.xml b/extensions/oidc-client/runtime/pom.xml index f79cd02ff1613..fba3263875f86 100644 --- a/extensions/oidc-client/runtime/pom.xml +++ b/extensions/oidc-client/runtime/pom.xml @@ -12,7 +12,7 @@ quarkus-oidc-client Quarkus - OpenID Connect Client - Runtime - Use OpenID Connect Client to get and refresh access tokens + Get and refresh access tokens from OpenID Connect providers io.quarkus diff --git a/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml index a09103cec0de7..a3c71c97581f0 100644 --- a/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -10,4 +10,4 @@ metadata: guide: "https://quarkus.io/guides/security-openid-connect-client" categories: - "security" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/oidc-token-propagation/runtime/pom.xml b/extensions/oidc-token-propagation/runtime/pom.xml index 1b9d5e48774f9..3f47c7e1982ea 100644 --- a/extensions/oidc-token-propagation/runtime/pom.xml +++ b/extensions/oidc-token-propagation/runtime/pom.xml @@ -12,7 +12,7 @@ quarkus-oidc-token-propagation Quarkus - OpenID Connect Token Propagation - Runtime - Use JAX-RS client filter to propagate the current access token as HTTP Authorization Bearer value + Use JAX-RS Client filter to propagate the incoming Bearer access token or token acquired from Authorization Code Flow as HTTP Authorization Bearer token io.quarkus diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java index b1a1274ddbc5d..cc02328f820fb 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -80,6 +80,7 @@ public class KeycloakDevServicesProcessor { private static volatile String capturedKeycloakUrl; private static volatile FileTime capturedRealmFileLastModifiedDate; private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); + private static volatile KeycloakDevServicesConfigBuildItem existingDevServiceConfig; @BuildStep(onlyIfNot = IsNormal.class, onlyIf = { IsEnabled.class, GlobalDevServicesConfig.Enabled.class }) public KeycloakDevServicesConfigBuildItem startKeycloakContainer( @@ -108,7 +109,7 @@ public KeycloakDevServicesConfigBuildItem startKeycloakContainer( } } if (!restartRequired) { - return null; + return existingDevServiceConfig; } for (Closeable closeable : closeables) { try { @@ -120,6 +121,7 @@ public KeycloakDevServicesConfigBuildItem startKeycloakContainer( closeables = null; capturedDevServicesConfiguration = null; capturedKeycloakUrl = null; + existingDevServiceConfig = null; } capturedDevServicesConfiguration = currentDevServicesConfiguration; @@ -199,7 +201,8 @@ private KeycloakDevServicesConfigBuildItem prepareConfiguration(boolean createRe configProperties.put(CLIENT_SECRET_CONFIG_KEY, oidcClientSecret); configProperties.put(OIDC_USERS, users); - return new KeycloakDevServicesConfigBuildItem(configProperties); + existingDevServiceConfig = new KeycloakDevServicesConfigBuildItem(configProperties); + return existingDevServiceConfig; } private StartResult startContainer(boolean useSharedContainer) { diff --git a/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html b/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html index 14779285ad747..3ab9b02730c08 100644 --- a/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html +++ b/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html @@ -178,7 +178,7 @@ userName = jsonPayload.preferred_username; } if (userName) { - $('#loggedInUser').append(" Logged in as " + userName); + $('#loggedInUser').append("Logged in as " + userName + " "); } } return "

" + 
@@ -289,8 +289,9 @@
                     Your tokens
                 
                 
             
diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java
index 1d55d0df736ee..ed0704e02aae7 100644
--- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java
+++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java
@@ -1,10 +1,12 @@
 package io.quarkus.oidc.test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
 import java.io.IOException;
+import java.net.URI;
 
 import org.jboss.shrinkwrap.api.ShrinkWrap;
 import org.jboss.shrinkwrap.api.spec.JavaArchive;
@@ -14,6 +16,8 @@
 import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
 import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
 import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.WebRequest;
+import com.gargoylesoftware.htmlunit.WebResponse;
 import com.gargoylesoftware.htmlunit.html.HtmlForm;
 import com.gargoylesoftware.htmlunit.html.HtmlPage;
 
@@ -87,6 +91,12 @@ public void testAccessAndRefreshTokenInjectionDevMode() throws IOException, Inte
 
             assertEquals("custom", page.getWebClient().getCookieManager().getCookie("q_session").getValue().split("\\|")[3]);
 
+            webClient.getOptions().setRedirectEnabled(false);
+            WebResponse webResponse = webClient
+                    .loadWebResponse(new WebRequest(URI.create("http://localhost:8080/protected/logout").toURL()));
+            assertEquals(302, webResponse.getStatusCode());
+            assertNull(webClient.getCookieManager().getCookie("q_session"));
+
             webClient.getCookieManager().clearCookies();
         }
     }
diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTokenStateManager.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTokenStateManager.java
index 220d89e1ff0ed..4a57c25770a12 100644
--- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTokenStateManager.java
+++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTokenStateManager.java
@@ -8,6 +8,7 @@
 import io.quarkus.oidc.OidcTenantConfig;
 import io.quarkus.oidc.TokenStateManager;
 import io.quarkus.oidc.runtime.DefaultTokenStateManager;
+import io.smallrye.mutiny.Uni;
 import io.vertx.ext.web.RoutingContext;
 
 @ApplicationScoped
@@ -18,25 +19,29 @@ public class CustomTokenStateManager implements TokenStateManager {
     DefaultTokenStateManager tokenStateManager;
 
     @Override
-    public String createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig,
-            AuthorizationCodeTokens sessionContent) {
-        return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent) + "|custom";
+    public Uni createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig,
+            AuthorizationCodeTokens sessionContent, TokenStateManager.CreateTokenStateRequestContext requestContext) {
+        return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent, requestContext)
+                .map(t -> (t + "|custom"));
     }
 
     @Override
-    public AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig,
-            String tokenState) {
+    public Uni getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig,
+            String tokenState, TokenStateManager.GetTokensRequestContext requestContext) {
         if (!tokenState.endsWith("|custom")) {
             throw new IllegalStateException();
         }
         String defaultState = tokenState.substring(0, tokenState.length() - 7);
-        return tokenStateManager.getTokens(routingContext, oidcConfig, defaultState);
+        return tokenStateManager.getTokens(routingContext, oidcConfig, defaultState, requestContext);
     }
 
     @Override
-    public void deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) {
+    public Uni deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState,
+            TokenStateManager.DeleteTokensRequestContext requestContext) {
         if (!tokenState.endsWith("|custom")) {
             throw new IllegalStateException();
         }
+        String defaultState = tokenState.substring(0, tokenState.length() - 7);
+        return tokenStateManager.deleteTokens(routingContext, oidcConfig, defaultState, requestContext);
     }
 }
diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java
index fa87a328bd5c8..9c1c73e03987b 100644
--- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java
+++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java
@@ -28,4 +28,10 @@ public String getName() {
     public String getTenantName(@PathParam("id") String tenantId) {
         return tenantId + ":" + idToken.getName();
     }
+
+    @GET
+    @Path("logout")
+    public void logout() {
+        throw new RuntimeException("Logout must be handled by CodeAuthenticationMechanism");
+    }
 }
diff --git a/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties b/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties
index 15b1c51e72f52..ee7c873dd6e01 100644
--- a/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties
+++ b/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties
@@ -5,6 +5,7 @@ quarkus.oidc.client-id=client-dev
 quarkus.oidc.credentials.client-secret.provider.name=vault-secret-provider
 quarkus.oidc.credentials.client-secret.provider.key=secret-from-vault
 quarkus.oidc.application-type=web-app
+quarkus.oidc.logout.path=/protected/logout
 
 quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL
 
diff --git a/extensions/oidc/runtime/pom.xml b/extensions/oidc/runtime/pom.xml
index 37e725258cb06..06a08cc1d7643 100644
--- a/extensions/oidc/runtime/pom.xml
+++ b/extensions/oidc/runtime/pom.xml
@@ -11,7 +11,7 @@
 
     quarkus-oidc
     Quarkus - OpenID Connect Adapter - Runtime
-    Secure your applications with OpenID Connect Adapter
+    Verify Bearer access tokens and authenticate users with Authorization Code Flow
     
         
             io.quarkus
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java
index c0f9143cef2f9..59fa463895eed 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java
@@ -21,10 +21,12 @@ public interface TenantConfigResolver {
      *
      * @param context the routing context
      * @return the tenant configuration. If {@code null}, indicates that the default configuration/tenant should be chosen
+     *
+     * @deprecated Use {@link #resolve(RoutingContext, TenantConfigRequestContext))} instead.
      */
     @Deprecated
     default OidcTenantConfig resolve(RoutingContext context) {
-        throw new UnsupportedOperationException("resolve not implemented");
+        throw new UnsupportedOperationException("resolve is not implemented");
     }
 
     /**
@@ -39,10 +41,10 @@ default Uni resolve(RoutingContext routingContext, TenantConfi
     }
 
     /**
-     * A context object that can be used to run blocking tasks
+     * A context object that can be used to run blocking tasks.
      * 

- * Blocking config providers should used this context object to run blocking tasks, to prevent excessive and - * unnecessary delegation to thread pools + * Blocking {@code TenantConfigResolver} providers should use this context object to run blocking tasks, to prevent + * excessive and unnecessary delegation to thread pools. */ interface TenantConfigRequestContext { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenStateManager.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenStateManager.java index 94cf60539cc73..ecda06f10e225 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenStateManager.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenStateManager.java @@ -1,5 +1,8 @@ package io.quarkus.oidc; +import java.util.function.Supplier; + +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; /** @@ -13,9 +16,125 @@ */ public interface TokenStateManager { - String createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, AuthorizationCodeTokens tokens); + /** + * Convert the authorization code flow tokens into a token state. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the authorization code flow tokens + * + * @return the token state + * + * @deprecated Use + * {@link #createTokenState(RoutingContext, OidcTenantConfig, AuthorizationCodeTokens, CreateTokenStateRequestContext)} + * + */ + @Deprecated + default String createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, + AuthorizationCodeTokens tokens) { + throw new UnsupportedOperationException("createTokenState is not implemented"); + } + + /** + * Convert the authorization code flow tokens into a token state. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the authorization code flow tokens + * @param requestContext the request context + * + * @return the token state + */ + default Uni createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, + AuthorizationCodeTokens tokens, CreateTokenStateRequestContext requestContext) { + return Uni.createFrom().item(createTokenState(routingContext, oidcConfig, tokens)); + } + + /** + * Convert the token state into the authorization code flow tokens. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the token state + * + * @return the authorization code flow tokens + * + * @deprecated Use {@link #getTokens(RoutingContext, OidcTenantConfig, String, GetTokensRequestContext)} instead. + */ + @Deprecated + default AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) { + throw new UnsupportedOperationException("getTokens is not implemented"); + } + + /** + * Convert the token state into the authorization code flow tokens. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the token state + * @param requestContext the request context + * + * @return the authorization code flow tokens + */ + default Uni getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, + String tokenState, GetTokensRequestContext requestContext) { + return Uni.createFrom().item(getTokens(routingContext, oidcConfig, tokenState)); + } + + /** + * Delete the token state. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the token state + * + * @deprecated Use {@link #deleteTokens(RoutingContext, OidcTenantConfig, String, DeleteTokensRequestContext)} instead + */ + @Deprecated + default void deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) { + throw new UnsupportedOperationException("deleteTokens is not implemented"); + } + + /** + * Delete the token state. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the token state + */ + default Uni deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, + DeleteTokensRequestContext requestContext) { + deleteTokens(routingContext, oidcConfig, tokenState); + return Uni.createFrom().voidItem(); + } + + /** + * A context object that can be used to create a token state by running a blocking task. + *

+ * Blocking providers should use this context to prevent excessive and unnecessary delegation to thread pools. + */ + interface CreateTokenStateRequestContext { + + Uni runBlocking(Supplier function); + } + + /** + * A context object that can be used to convert the token state to the tokens by running a blocking task. + *

+ * Blocking providers should use this context to prevent excessive and unnecessary delegation to thread pools. + */ + interface GetTokensRequestContext { + + Uni runBlocking(Supplier function); + } - AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState); + /** + * A context object that can be used to delete the token state by running a blocking task. + *

+ * Blocking providers should use this context to prevent excessive and unnecessary delegation to thread pools. + */ + interface DeleteTokensRequestContext { - void deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState); + Uni runBlocking(Supplier function); + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index cb4bb8ccd7701..8e6e5e656fa49 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -10,7 +10,9 @@ import java.util.Optional; import java.util.UUID; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import java.util.regex.Pattern; import org.jboss.logging.Logger; @@ -23,8 +25,11 @@ import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.OidcTenantConfig.Authentication; import io.quarkus.oidc.SecurityEvent; +import io.quarkus.oidc.TokenStateManager; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.runtime.BlockingOperationControl; +import io.quarkus.runtime.ExecutorRecorder; import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; @@ -32,6 +37,7 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.UniEmitter; import io.vertx.core.http.Cookie; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.impl.CookieImpl; @@ -47,12 +53,17 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha static final Pattern COOKIE_PATTERN = Pattern.compile("\\" + COOKIE_DELIM); static final String SESSION_COOKIE_NAME = "q_session"; static final String SESSION_MAX_AGE_PARAM = "session-max-age"; + static final Uni VOID_UNI = Uni.createFrom().voidItem(); private static final Logger LOG = Logger.getLogger(CodeAuthenticationMechanism.class); private static final String STATE_COOKIE_NAME = "q_auth"; private static final String POST_LOGOUT_COOKIE_NAME = "q_post_logout"; + private final CreateTokenStateRequestContext createTokenStateRequestContext = new CreateTokenStateRequestContext(); + private final GetTokensRequestContext getTokenStateRequestContext = new GetTokensRequestContext(); + private final DeleteTokensRequestContext deleteTokensRequestContext = new DeleteTokensRequestContext(); + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { return resolver.resolveConfig(context).chain(new Function>() { @@ -96,51 +107,60 @@ private Uni reAuthenticate(Cookie sessionCookie, IdentityProviderManager identityProviderManager, TenantConfigContext configContext) { - AuthorizationCodeTokens session = resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig, - sessionCookie.getValue()); - - context.put(OidcConstants.ACCESS_TOKEN_VALUE, session.getAccessToken()); - context.put(AuthorizationCodeTokens.class.getName(), session); - return authenticate(identityProviderManager, context, new IdTokenCredential(session.getIdToken(), context)) - .map(new Function() { - @Override - public SecurityIdentity apply(SecurityIdentity identity) { - if (isLogout(context, configContext)) { - fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity); - throw redirectToLogoutEndpoint(context, configContext, session.getIdToken()); - } - return identity; - } - }).onFailure().recoverWithUni(new Function>() { + return resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig, + sessionCookie.getValue(), getTokenStateRequestContext) + .chain(new Function>() { @Override - public Uni apply(Throwable t) { - if (t instanceof AuthenticationRedirectException) { - throw (AuthenticationRedirectException) t; - } + public Uni apply(AuthorizationCodeTokens session) { + context.put(OidcConstants.ACCESS_TOKEN_VALUE, session.getAccessToken()); + context.put(AuthorizationCodeTokens.class.getName(), session); + return authenticate(identityProviderManager, context, + new IdTokenCredential(session.getIdToken(), context)) + .call(new Function>() { + @Override + public Uni apply(SecurityIdentity identity) { + if (isLogout(context, configContext)) { + fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity); + return buildLogoutRedirectUriUni(context, configContext, + session.getIdToken()); + } + return VOID_UNI; + } + }).onFailure() + .recoverWithUni(new Function>() { + @Override + public Uni apply(Throwable t) { + if (t instanceof AuthenticationRedirectException) { + throw (AuthenticationRedirectException) t; + } - if (!(t instanceof TokenAutoRefreshException)) { - boolean expired = (t.getCause() instanceof InvalidJwtException) - && ((InvalidJwtException) t.getCause()).hasErrorCode(ErrorCodes.EXPIRED); + if (!(t instanceof TokenAutoRefreshException)) { + boolean expired = (t.getCause() instanceof InvalidJwtException) + && ((InvalidJwtException) t.getCause()) + .hasErrorCode(ErrorCodes.EXPIRED); - if (!expired) { - LOG.debugf("Authentication failure: %s", t.getCause()); - throw new AuthenticationCompletionException(t.getCause()); - } - if (!configContext.oidcConfig.token.refreshExpired) { - LOG.debug("Token has expired, token refresh is not allowed"); - throw new AuthenticationCompletionException(t.getCause()); - } - LOG.debug("Token has expired, trying to refresh it"); - return refreshSecurityIdentity(configContext, session.getRefreshToken(), context, - identityProviderManager, false, null); - } else { - return refreshSecurityIdentity(configContext, session.getRefreshToken(), context, - identityProviderManager, true, - ((TokenAutoRefreshException) t).getSecurityIdentity()); - } + if (!expired) { + LOG.debugf("Authentication failure: %s", t.getCause()); + throw new AuthenticationCompletionException(t.getCause()); + } + if (!configContext.oidcConfig.token.refreshExpired) { + LOG.debug("Token has expired, token refresh is not allowed"); + throw new AuthenticationCompletionException(t.getCause()); + } + LOG.debug("Token has expired, trying to refresh it"); + return refreshSecurityIdentity(configContext, session.getRefreshToken(), + context, + identityProviderManager, false, null); + } else { + return refreshSecurityIdentity(configContext, session.getRefreshToken(), + context, + identityProviderManager, true, + ((TokenAutoRefreshException) t).getSecurityIdentity()); + } + } + }); } }); - } private boolean isJavaScript(RoutingContext context) { @@ -168,54 +188,63 @@ public Uni apply(TenantConfigContext tenantContext) { } public Uni getChallengeInternal(RoutingContext context, TenantConfigContext configContext) { - removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)); - - if (!shouldAutoRedirect(configContext, context)) { - // If the client (usually an SPA) wants to handle the redirect manually, then - // return status code 499 and WWW-Authenticate header with the 'OIDC' value. - return Uni.createFrom().item(new ChallengeData(499, "WWW-Authenticate", "OIDC")); - } - - StringBuilder codeFlowParams = new StringBuilder(); - - // response_type - codeFlowParams.append(OidcConstants.CODE_FLOW_RESPONSE_TYPE).append(EQ).append(OidcConstants.CODE_FLOW_CODE); - - // client_id - codeFlowParams.append(AMP).append(OidcConstants.CLIENT_ID).append(EQ) - .append(OidcCommonUtils.urlEncode(configContext.oidcConfig.clientId.get())); - - // scope - List scopes = new ArrayList<>(); - scopes.add("openid"); - configContext.oidcConfig.getAuthentication().scopes.ifPresent(scopes::addAll); - codeFlowParams.append(AMP).append(OidcConstants.TOKEN_SCOPE).append(EQ) - .append(OidcCommonUtils.urlEncode(String.join(" ", scopes))); - - // redirect_uri - String redirectPath = getRedirectPath(configContext, context); - String redirectUriParam = buildUri(context, isForceHttps(configContext), redirectPath); - LOG.debugf("Authentication request redirect_uri parameter: %s", redirectUriParam); + return removeSessionCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)) + .chain(new Function>() { - codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_REDIRECT_URI).append(EQ) - .append(OidcCommonUtils.urlEncode(redirectUriParam)); + @Override + public Uni apply(Void t) { + if (!shouldAutoRedirect(configContext, context)) { + // If the client (usually an SPA) wants to handle the redirect manually, then + // return status code 499 and WWW-Authenticate header with the 'OIDC' value. + return Uni.createFrom().item(new ChallengeData(499, "WWW-Authenticate", "OIDC")); + } - // state - codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_STATE).append(EQ) - .append(generateCodeFlowState(context, configContext, redirectPath)); + StringBuilder codeFlowParams = new StringBuilder(); + + // response_type + codeFlowParams.append(OidcConstants.CODE_FLOW_RESPONSE_TYPE).append(EQ) + .append(OidcConstants.CODE_FLOW_CODE); + + // client_id + codeFlowParams.append(AMP).append(OidcConstants.CLIENT_ID).append(EQ) + .append(OidcCommonUtils.urlEncode(configContext.oidcConfig.clientId.get())); + + // scope + List scopes = new ArrayList<>(); + scopes.add("openid"); + configContext.oidcConfig.getAuthentication().scopes.ifPresent(scopes::addAll); + codeFlowParams.append(AMP).append(OidcConstants.TOKEN_SCOPE).append(EQ) + .append(OidcCommonUtils.urlEncode(String.join(" ", scopes))); + + // redirect_uri + String redirectPath = getRedirectPath(configContext, context); + String redirectUriParam = buildUri(context, isForceHttps(configContext), redirectPath); + LOG.debugf("Authentication request redirect_uri parameter: %s", redirectUriParam); + + codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_REDIRECT_URI).append(EQ) + .append(OidcCommonUtils.urlEncode(redirectUriParam)); + + // state + codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_STATE).append(EQ) + .append(generateCodeFlowState(context, configContext, redirectPath)); + + // extra redirect parameters, see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequests + if (configContext.oidcConfig.authentication.getExtraParams() != null) { + for (Map.Entry entry : configContext.oidcConfig.authentication.getExtraParams() + .entrySet()) { + codeFlowParams.append(AMP).append(entry.getKey()).append(EQ) + .append(OidcCommonUtils.urlEncode(entry.getValue())); + } + } - // extra redirect parameters, see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequests - if (configContext.oidcConfig.authentication.getExtraParams() != null) { - for (Map.Entry entry : configContext.oidcConfig.authentication.getExtraParams().entrySet()) { - codeFlowParams.append(AMP).append(entry.getKey()).append(EQ) - .append(OidcCommonUtils.urlEncode(entry.getValue())); - } - } + String authorizationURL = configContext.provider.getMetadata().getAuthorizationUri() + "?" + + codeFlowParams.toString(); - String authorizationURL = configContext.provider.getMetadata().getAuthorizationUri() + "?" + codeFlowParams.toString(); + return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION, + authorizationURL)); + } - return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION, - authorizationURL)); + }); } private Uni performCodeFlow(IdentityProviderManager identityProviderManager, @@ -278,12 +307,16 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T return authenticate(identityProviderManager, context, new IdTokenCredential(tokens.getIdToken(), context)) + .call(new Function>() { + @Override + public Uni apply(SecurityIdentity identity) { + return processSuccessfulAuthentication(context, configContext, + tokens, identity); + } + }) .map(new Function() { @Override public SecurityIdentity apply(SecurityIdentity identity) { - processSuccessfulAuthentication(context, configContext, - tokens, identity); - boolean removeRedirectParams = configContext.oidcConfig.authentication .isRemoveRedirectParameters(); if (removeRedirectParams || finalUserPath != null @@ -325,31 +358,48 @@ public Throwable apply(Throwable tInner) { } - private void processSuccessfulAuthentication(RoutingContext context, + private Uni processSuccessfulAuthentication(RoutingContext context, TenantConfigContext configContext, AuthorizationCodeTokens tokens, SecurityIdentity securityIdentity) { - removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)); + return removeSessionCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)) + .chain(new Function>() { - JsonObject idToken = OidcUtils.decodeJwtContent(tokens.getIdToken()); + @Override + public Uni apply(Void t) { + JsonObject idToken = OidcUtils.decodeJwtContent(tokens.getIdToken()); - if (!idToken.containsKey("exp") || !idToken.containsKey("iat")) { - LOG.debug("ID Token is required to contain 'exp' and 'iat' claims"); - throw new AuthenticationCompletionException(); - } - long maxAge = idToken.getLong("exp") - idToken.getLong("iat"); - if (configContext.oidcConfig.token.lifespanGrace.isPresent()) { - maxAge += configContext.oidcConfig.token.lifespanGrace.getAsInt(); - } - if (configContext.oidcConfig.token.refreshExpired) { - maxAge += configContext.oidcConfig.authentication.sessionAgeExtension.getSeconds(); - } - context.put(SESSION_MAX_AGE_PARAM, maxAge); - String cookieValue = resolver.getTokenStateManager() - .createTokenState(context, configContext.oidcConfig, tokens); - createCookie(context, configContext.oidcConfig, getSessionCookieName(configContext.oidcConfig), cookieValue, maxAge); + if (!idToken.containsKey("exp") || !idToken.containsKey("iat")) { + LOG.debug("ID Token is required to contain 'exp' and 'iat' claims"); + throw new AuthenticationCompletionException(); + } + long maxAge = idToken.getLong("exp") - idToken.getLong("iat"); + if (configContext.oidcConfig.token.lifespanGrace.isPresent()) { + maxAge += configContext.oidcConfig.token.lifespanGrace.getAsInt(); + } + if (configContext.oidcConfig.token.refreshExpired) { + maxAge += configContext.oidcConfig.authentication.sessionAgeExtension.getSeconds(); + } + final long sessionMaxAge = maxAge; + context.put(SESSION_MAX_AGE_PARAM, maxAge); + return resolver.getTokenStateManager() + .createTokenState(context, configContext.oidcConfig, tokens, createTokenStateRequestContext) + .map(new Function() { + + @Override + public Void apply(String cookieValue) { + createCookie(context, configContext.oidcConfig, + getSessionCookieName(configContext.oidcConfig), + cookieValue, sessionMaxAge); + fireEvent(SecurityEvent.Type.OIDC_LOGIN, securityIdentity); + return null; + } + + }); + } + + }); - fireEvent(SecurityEvent.Type.OIDC_LOGIN, securityIdentity); } private void fireEvent(SecurityEvent.Type eventType, SecurityIdentity securityIdentity) { @@ -441,14 +491,24 @@ private String buildUri(RoutingContext context, boolean forceHttps, String autho .toString(); } - private void removeCookie(RoutingContext context, TenantConfigContext configContext, String cookieName) { + private Uni removeSessionCookie(RoutingContext context, TenantConfigContext configContext, String cookieName) { + String cookieValue = removeCookie(context, configContext, cookieName); + if (cookieValue != null) { + return resolver.getTokenStateManager().deleteTokens(context, configContext.oidcConfig, cookieValue, + deleteTokensRequestContext); + } else { + return VOID_UNI; + } + } + + private String removeCookie(RoutingContext context, TenantConfigContext configContext, String cookieName) { ServerCookie cookie = (ServerCookie) context.cookieMap().get(cookieName); + String cookieValue = null; if (cookie != null) { - if (SESSION_COOKIE_NAME.equals(cookieName)) { - resolver.getTokenStateManager().deleteTokens(context, configContext.oidcConfig, cookie.getValue()); - } + cookieValue = cookie.getValue(); removeCookie(context, cookie, configContext.oidcConfig); } + return cookieValue; } static void removeCookie(RoutingContext context, ServerCookie cookie, OidcTenantConfig oidcConfig) { @@ -500,12 +560,17 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T return authenticate(identityProviderManager, context, new IdTokenCredential(tokens.getIdToken(), context)) - .map(new Function() { + .call(new Function>() { @Override - public SecurityIdentity apply(SecurityIdentity identity) { + public Uni apply(SecurityIdentity identity) { // after a successful refresh, rebuild the identity and update the cookie - processSuccessfulAuthentication(context, configContext, + return processSuccessfulAuthentication(context, configContext, tokens, identity); + } + }) + .map(new Function() { + @Override + public SecurityIdentity apply(SecurityIdentity identity) { fireEvent(autoRefresh ? SecurityEvent.Type.OIDC_SESSION_REFRESHED : SecurityEvent.Type.OIDC_SESSION_EXPIRED_AND_REFRESHED, identity); @@ -555,10 +620,15 @@ private boolean isForceHttps(TenantConfigContext configContext) { return configContext.oidcConfig.authentication.forceRedirectHttpsScheme; } - private AuthenticationRedirectException redirectToLogoutEndpoint(RoutingContext context, TenantConfigContext configContext, + private Uni buildLogoutRedirectUriUni(RoutingContext context, TenantConfigContext configContext, String idToken) { - removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)); - return new AuthenticationRedirectException(buildLogoutRedirectUri(configContext, idToken, context)); + return removeSessionCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)) + .map(new Function() { + @Override + public Void apply(Void t) { + throw new AuthenticationRedirectException(buildLogoutRedirectUri(configContext, idToken, context)); + } + }); } private static String getStateCookieName(TenantConfigContext configContext) { @@ -580,4 +650,48 @@ static String getCookieSuffix(String tenantId) { return !"Default".equals(tenantId) ? "_" + tenantId : ""; } + private static class CreateTokenStateRequestContext extends BlockingTaskRunner + implements TokenStateManager.CreateTokenStateRequestContext { + } + + private static class GetTokensRequestContext extends BlockingTaskRunner + implements TokenStateManager.GetTokensRequestContext { + } + + private static class DeleteTokensRequestContext extends BlockingTaskRunner + implements TokenStateManager.DeleteTokensRequestContext { + } + + private static class BlockingTaskRunner { + public Uni runBlocking(Supplier function) { + return Uni.createFrom().deferred(new Supplier>() { + @Override + public Uni get() { + if (BlockingOperationControl.isBlockingAllowed()) { + try { + return Uni.createFrom().item(function.get()); + } catch (Throwable t) { + return Uni.createFrom().failure(t); + } + } else { + return Uni.createFrom().emitter(new Consumer>() { + @Override + public void accept(UniEmitter uniEmitter) { + ExecutorRecorder.getCurrent().execute(new Runnable() { + @Override + public void run() { + try { + uniEmitter.complete(function.get()); + } catch (Throwable t) { + uniEmitter.fail(t); + } + } + }); + } + }); + } + } + }); + } + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java index 2b0a951258c24..b5f05b044882d 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java @@ -5,6 +5,7 @@ import io.quarkus.oidc.AuthorizationCodeTokens; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.TokenStateManager; +import io.smallrye.mutiny.Uni; import io.vertx.core.http.Cookie; import io.vertx.core.http.impl.ServerCookie; import io.vertx.ext.web.RoutingContext; @@ -16,8 +17,8 @@ public class DefaultTokenStateManager implements TokenStateManager { private static final String SESSION_RT_COOKIE_NAME = CodeAuthenticationMechanism.SESSION_COOKIE_NAME + "_rt"; @Override - public String createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, - AuthorizationCodeTokens tokens) { + public Uni createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, + AuthorizationCodeTokens tokens, TokenStateManager.CreateTokenStateRequestContext requestContext) { StringBuilder sb = new StringBuilder(); sb.append(tokens.getIdToken()); if (oidcConfig.tokenStateManager.strategy == OidcTenantConfig.TokenStateManager.Strategy.KEEP_ALL_TOKENS) { @@ -56,11 +57,12 @@ public String createTokenState(RoutingContext routingContext, OidcTenantConfig o } } } - return sb.toString(); + return Uni.createFrom().item(sb.toString()); } @Override - public AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) { + public Uni getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, + TokenStateManager.GetTokensRequestContext requestContext) { String[] tokens = CodeAuthenticationMechanism.COOKIE_PATTERN.split(tokenState); String idToken = tokens[0]; @@ -91,17 +93,19 @@ public AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTena } } - return new AuthorizationCodeTokens(idToken, accessToken, refreshToken); + return Uni.createFrom().item(new AuthorizationCodeTokens(idToken, accessToken, refreshToken)); } @Override - public void deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) { + public Uni deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, + TokenStateManager.DeleteTokensRequestContext requestContext) { if (oidcConfig.tokenStateManager.splitTokens) { CodeAuthenticationMechanism.removeCookie(routingContext, getAccessTokenCookie(routingContext, oidcConfig), oidcConfig); CodeAuthenticationMechanism.removeCookie(routingContext, getRefreshTokenCookie(routingContext, oidcConfig), oidcConfig); } + return CodeAuthenticationMechanism.VOID_UNI; } private static ServerCookie getAccessTokenCookie(RoutingContext routingContext, OidcTenantConfig oidcConfig) { diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/restclient/ClientTracingFilter.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/restclient/ClientTracingFilter.java index 9d34efb149b28..5f43898887b75 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/restclient/ClientTracingFilter.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/restclient/ClientTracingFilter.java @@ -26,7 +26,7 @@ @Priority(Priorities.HEADER_DECORATOR) public class ClientTracingFilter implements ClientRequestFilter, ClientResponseFilter { - private static final TextMapPropagator TEXT_MAP_PROPAGATOR = GlobalOpenTelemetry.getPropagators().getTextMapPropagator(); + private final TextMapPropagator TEXT_MAP_PROPAGATOR = GlobalOpenTelemetry.getPropagators().getTextMapPropagator(); private static final String SCOPE_KEY = ClientTracingFilter.class.getName() + ".scope"; private static final String SPAN_KEY = ClientTracingFilter.class.getName() + ".span"; diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java index 16b3161928558..16cf244beafc9 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.deployment; +import java.util.List; import java.util.Optional; import java.util.regex.Pattern; @@ -18,6 +19,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; +import io.quarkus.resteasy.common.spi.ResteasyDotNames; import io.quarkus.resteasy.runtime.QuarkusRestPathTemplate; import io.quarkus.resteasy.runtime.QuarkusRestPathTemplateInterceptor; import io.quarkus.resteasy.server.common.spi.ResteasyJaxrsConfigBuildItem; @@ -81,14 +83,22 @@ public void transform(TransformationContext ctx) { AnnotationInstance annotation = methodInfo.annotation(REST_PATH); if (annotation == null) { - return; + // Check for @Path on class and not method + if (!isRestEndpointMethod(methodInfo.annotations())) { + return; + } } // Don't create annotations for rest clients if (classInfo.classAnnotation(REGISTER_REST_CLIENT) != null) { return; } - StringBuilder stringBuilder = new StringBuilder(slashify(annotation.value().asString())); + StringBuilder stringBuilder; + if (annotation != null) { + stringBuilder = new StringBuilder(slashify(annotation.value().asString())); + } else { + stringBuilder = new StringBuilder(); + } // Look for @Path annotation on the class annotation = classInfo.classAnnotation(REST_PATH); @@ -128,6 +138,19 @@ String slashify(String path) { return '/' + path; } + boolean isRestEndpointMethod(List annotations) { + boolean isRestEndpointMethod = false; + + for (AnnotationInstance annotation : annotations) { + if (ResteasyDotNames.JAXRS_METHOD_ANNOTATIONS.contains(annotation.name())) { + isRestEndpointMethod = true; + break; + } + } + + return isRestEndpointMethod; + } + private boolean notRequired(Capabilities capabilities, Optional metricsCapability) { return capabilities.isMissing(Capability.RESTEASY) || diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 815441cbad750..e843d538face9 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -146,6 +146,9 @@ public class JaxrsClientReactiveProcessor { String.class, Object.class); private static final MethodDescriptor MULTIVALUED_MAP_ADD = MethodDescriptor.ofMethod(MultivaluedMap.class, "add", void.class, Object.class, Object.class); + private static final MethodDescriptor PATH_GET_FILENAME = MethodDescriptor.ofMethod(Path.class, "getFileName", + Path.class); + private static final MethodDescriptor OBJECT_TO_STRING = MethodDescriptor.ofMethod(Object.class, "toString", String.class); static final DotName CONTINUATION = DotName.createSimple("kotlin.coroutines.Continuation"); private static final DotName UNI_KT = DotName.createSimple("io.smallrye.mutiny.coroutines.UniKt"); @@ -502,7 +505,11 @@ A more full example of generated client (with sub-resource) can is at the bottom ClassInfo subResourceInterface = index.getClassByName(returnType.name()); if (!Modifier.isInterface(subResourceInterface.flags())) { throw new IllegalArgumentException( - "Sub resource type is not an interface: " + returnType.name().toString()); + "Client interface method: " + jandexMethod.declaringClass().name() + "#" + jandexMethod + + " has no HTTP method annotation (@GET, @POST, etc) and it's return type: " + + returnType.name().toString() + " is not an interface. " + + "If it's a sub resource method, it has to return an interface. " + + "If it's not, it has to have one of the HTTP method annotations."); } // generate implementation for a method from the jaxrs interface: MethodCreator methodCreator = c.getMethodCreator(method.getName(), method.getSimpleReturnType(), @@ -989,7 +996,7 @@ private ResultHandle createMultipartForm(MethodCreator methodCreator, ResultHand formClass.name() + "." + field.name()); } ResultHandle filePath = methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(File.class, "getPath", String.class), fieldValue); + MethodDescriptor.ofMethod(File.class, "toPath", Path.class), fieldValue); addFile(methodCreator, multipartForm, formParamName, partType, filePath); } else if (is(PATH, fieldClass, index)) { // and so is path @@ -998,9 +1005,7 @@ private ResultHandle createMultipartForm(MethodCreator methodCreator, ResultHand "No @PartType annotation found on multipart form field of type Path: " + formClass.name() + "." + field.name()); } - ResultHandle filePath = methodCreator.invokeInterfaceMethod( - MethodDescriptor.ofMethod(Path.class, "toString", String.class), fieldValue); - addFile(methodCreator, multipartForm, formParamName, partType, filePath); + addFile(methodCreator, multipartForm, formParamName, partType, fieldValue); } else if (is(BUFFER, fieldClass, index)) { // and buffer addBuffer(methodCreator, multipartForm, formParamName, partType, fieldValue, field); @@ -1047,6 +1052,9 @@ private ResultHandle createMultipartForm(MethodCreator methodCreator, ResultHand */ private void addFile(MethodCreator methodCreator, AssignableResultHandle multipartForm, String formParamName, String partType, ResultHandle filePath) { + ResultHandle fileNamePath = methodCreator.invokeInterfaceMethod(PATH_GET_FILENAME, filePath); + ResultHandle fileName = methodCreator.invokeVirtualMethod(OBJECT_TO_STRING, fileNamePath); + ResultHandle pathString = methodCreator.invokeVirtualMethod(OBJECT_TO_STRING, filePath); if (partType.equalsIgnoreCase(MediaType.APPLICATION_OCTET_STREAM)) { methodCreator.assign(multipartForm, // MultipartForm#binaryFileUpload(String name, String filename, String pathname, String mediaType); @@ -1055,8 +1063,8 @@ private void addFile(MethodCreator methodCreator, AssignableResultHandle multipa MethodDescriptor.ofMethod(MultipartForm.class, "binaryFileUpload", MultipartForm.class, String.class, String.class, String.class, String.class), - multipartForm, methodCreator.load(formParamName), methodCreator.load(formParamName), - filePath, methodCreator.load(partType))); + multipartForm, methodCreator.load(formParamName), fileName, + pathString, methodCreator.load(partType))); } else { methodCreator.assign(multipartForm, // MultipartForm#textFileUpload(String name, String filename, String pathname, String mediaType);; @@ -1065,8 +1073,8 @@ private void addFile(MethodCreator methodCreator, AssignableResultHandle multipa MethodDescriptor.ofMethod(MultipartForm.class, "textFileUpload", MultipartForm.class, String.class, String.class, String.class, String.class), - multipartForm, methodCreator.load(formParamName), methodCreator.load(formParamName), - filePath, methodCreator.load(partType))); + multipartForm, methodCreator.load(formParamName), fileName, + pathString, methodCreator.load(partType))); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ArcBeanFactory.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ArcBeanFactory.java index 0808558c0ca57..c44b8bb45458c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ArcBeanFactory.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ArcBeanFactory.java @@ -22,17 +22,28 @@ public String toString() { @Override public BeanInstance createInstance() { - BeanContainer.Instance instance = factory.create(); - return new BeanInstance() { - @Override - public T getInstance() { - return instance.get(); + BeanContainer.Instance instance; + try { + instance = factory.create(); + return new BeanInstance() { + @Override + public T getInstance() { + return instance.get(); + } + + @Override + public void close() { + instance.close(); + } + }; + } catch (Exception e) { + if (factory.getClass().getName().contains("DefaultInstanceFactory")) { + throw new IllegalArgumentException( + "Unable to create class '" + targetClassName + + "'. To fix the problem, make sure this class is a CDI bean.", + e); } - - @Override - public void close() { - instance.close(); - } - }; + throw e; + } } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java index e02091633f902..b9353e698a05e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java @@ -189,7 +189,8 @@ private MultipartPopulatorGenerator() { */ static String generate(ClassInfo multipartClassInfo, ClassOutput classOutput, IndexView index) { if (!multipartClassInfo.hasNoArgsConstructor()) { - throw new IllegalArgumentException("Classes annotated with '@MultipartForm' must contain a no-args constructor"); + throw new IllegalArgumentException("Classes annotated with '@MultipartForm' must contain a no-args constructor. " + + "The constructor is missing on " + multipartClassInfo.name()); } String multipartClassName = multipartClassInfo.name().toString(); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index d9b37a93faab0..62fac03831ff8 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -10,11 +10,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; import javax.ws.rs.Priorities; @@ -41,13 +41,13 @@ import org.jboss.resteasy.reactive.common.model.ResourceDynamicFeature; import org.jboss.resteasy.reactive.common.model.ResourceFeature; import org.jboss.resteasy.reactive.common.model.ResourceInterceptors; -import org.jboss.resteasy.reactive.common.model.ResourceMethod; import org.jboss.resteasy.reactive.common.model.ResourceReader; import org.jboss.resteasy.reactive.common.model.ResourceWriter; import org.jboss.resteasy.reactive.common.processor.AdditionalReaderWriter; import org.jboss.resteasy.reactive.common.processor.AdditionalReaders; import org.jboss.resteasy.reactive.common.processor.AdditionalWriters; import org.jboss.resteasy.reactive.common.processor.DefaultProducesHandler; +import org.jboss.resteasy.reactive.common.processor.EndpointIndexer; 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.ResourceScanningResult; @@ -335,7 +335,7 @@ public void setupEndpoints(Capabilities capabilities, BeanArchiveIndexBuildItem QuarkusServerEndpointIndexer.Builder serverEndpointIndexerBuilder = new QuarkusServerEndpointIndexer.Builder() .addMethodScanners( - methodScanners.stream().map(MethodScannerBuildItem::getMethodScanner).collect(Collectors.toList())) + methodScanners.stream().map(MethodScannerBuildItem::getMethodScanner).collect(toList())) .setIndex(index) .setFactoryCreator(new QuarkusFactoryCreator(recorder, beanContainerBuildItem.getValue())) .setEndpointInvokerFactory(new QuarkusInvokerFactory(generatedClassBuildItemBuildProducer, recorder)) @@ -353,13 +353,20 @@ public void setupEndpoints(Capabilities capabilities, BeanArchiveIndexBuildItem .setClassLevelExceptionMappers( classLevelExceptionMappers.isPresent() ? classLevelExceptionMappers.get().getMappers() : Collections.emptyMap()) - .setResourceMethodCallback(new Consumer>() { + .setResourceMethodCallback(new Consumer<>() { @Override - public void accept(Map.Entry entry) { - MethodInfo method = entry.getKey(); + public void accept(EndpointIndexer.ResourceMethodCallbackData entry) { + MethodInfo method = entry.getMethodInfo(); String source = ResteasyReactiveProcessor.class.getSimpleName() + " > " + method.declaringClass() + "[" + method + "]"; + ClassInfo classInfoWithSecurity = consumeStandardSecurityAnnotations(method, + entry.getActualEndpointInfo(), index, c -> c); + if (classInfoWithSecurity != null) { + reflectiveClass.produce(new ReflectiveClassBuildItem(false, true, false, + entry.getActualEndpointInfo().name().toString())); + } + reflectiveHierarchy.produce(new ReflectiveHierarchyBuildItem.Builder() .type(method.returnType()) .index(index) @@ -648,21 +655,29 @@ MethodScannerBuildItem integrateEagerSecurity(Capabilities capabilities, Combine @Override public List scan(MethodInfo method, ClassInfo actualEndpointClass, Map methodContext) { - if (SecurityTransformerUtils.hasStandardSecurityAnnotation(method)) { - return Collections.singletonList(new EagerSecurityHandler.Customizer()); - } - ClassInfo c = actualEndpointClass; - while (c.superName() != null) { - if (SecurityTransformerUtils.hasStandardSecurityAnnotation(c)) { - return Collections.singletonList(new EagerSecurityHandler.Customizer()); - } - c = index.getClassByName(c.superName()); - } - return Collections.emptyList(); + return Objects.requireNonNullElse( + consumeStandardSecurityAnnotations(method, actualEndpointClass, index, + (c) -> Collections.singletonList(new EagerSecurityHandler.Customizer())), + Collections.emptyList()); } }); } + private T consumeStandardSecurityAnnotations(MethodInfo methodInfo, ClassInfo classInfo, IndexView index, + Function function) { + if (SecurityTransformerUtils.hasStandardSecurityAnnotation(methodInfo)) { + return function.apply(methodInfo.declaringClass()); + } + ClassInfo c = classInfo; + while (c.superName() != null) { + if (SecurityTransformerUtils.hasStandardSecurityAnnotation(c)) { + return function.apply(c); + } + c = index.getClassByName(c.superName()); + } + return null; + } + private Optional getAppPath(Optional newPropertyValue) { Optional legacyProperty = ConfigProvider.getConfig().getOptionalValue("quarkus.rest.path", String.class); if (legacyProperty.isPresent()) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/cache/NoCacheOnMethodsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/cache/NoCacheOnMethodsTest.java index 00662d7c38287..1925a3855d852 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/cache/NoCacheOnMethodsTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/cache/NoCacheOnMethodsTest.java @@ -30,37 +30,53 @@ public JavaArchive get() { }); @Test - public void testWith() { - RestAssured.get("/test/with") + public void testWithFields() { + RestAssured.get("/test/withFields") .then() .statusCode(200) - .body(equalTo("with")) + .body(equalTo("withFields")) .header("Cache-Control", "no-cache=\"f1\", no-cache=\"f2\""); } @Test - public void testWithout() { - RestAssured.get("/test/without") + public void testWithoutFields() { + RestAssured.get("/test/withoutFields") .then() .statusCode(200) - .body(equalTo("without")) + .body(equalTo("withoutFields")) + .header("Cache-Control", "no-cache"); + } + + @Test + public void testWithoutAnnotation() { + RestAssured.get("/test/withoutAnnotation") + .then() + .statusCode(200) + .body(equalTo("withoutAnnotation")) .header("Cache-Control", nullValue()); } @Path("test") public static class ResourceWithNoCache { - @Path("with") + @Path("withFields") @GET @NoCache(fields = { "f1", "f2" }) - public String with() { - return "with"; + public String withFields() { + return "withFields"; + } + + @Path("withoutFields") + @GET + @NoCache + public String withoutFields() { + return "withoutFields"; } - @Path("without") + @Path("withoutAnnotation") @GET - public String without() { - return "without"; + public String withoutAnnotation() { + return "withoutAnnotation"; } } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java index 5489e49172b3f..225943678fa69 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java @@ -39,7 +39,7 @@ public JavaArchive get() { return ShrinkWrap.create(JavaArchive.class) .addClasses(Resource.class, Data.class) .addAsResource(new StringAsset( - "quarkus.http.limits.max-form-attribute-size=4K"), + "quarkus.http.limits.max-form-attribute-size=120K"), "application.properties"); } }); @@ -49,6 +49,12 @@ public JavaArchive get() { @Test public void test() throws IOException { String fileContents = new String(Files.readAllBytes(FILE.toPath()), StandardCharsets.UTF_8); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; ++i) { + sb.append(fileContents); + } + fileContents = sb.toString(); + Assertions.assertTrue(fileContents.length() > HttpServerOptions.DEFAULT_MAX_FORM_ATTRIBUTE_SIZE); given() .multiPart("text", fileContents) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java index 6699beab8c705..ef3d755319168 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java @@ -66,9 +66,13 @@ public void clearDirectory() { @Test public void test() throws IOException { - String formAttrSourceFileContents = new String(Files.readAllBytes(FORM_ATTR_SOURCE_FILE.toPath()), - StandardCharsets.UTF_8); - Assertions.assertTrue(formAttrSourceFileContents.length() > HttpServerOptions.DEFAULT_MAX_FORM_ATTRIBUTE_SIZE); + String fileContents = new String(Files.readAllBytes(FORM_ATTR_SOURCE_FILE.toPath()), StandardCharsets.UTF_8); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; ++i) { + sb.append(fileContents); + } + fileContents = sb.toString(); + Assertions.assertTrue(fileContents.length() > HttpServerOptions.DEFAULT_MAX_FORM_ATTRIBUTE_SIZE); given() .multiPart("active", "true") .multiPart("num", "25") @@ -76,7 +80,7 @@ public void test() throws IOException { .multiPart("htmlFile", HTML_FILE, "text/html") .multiPart("xmlFile", XML_FILE, "text/xml") .multiPart("txtFile", TXT_FILE, "text/plain") - .multiPart("name", formAttrSourceFileContents) + .multiPart("name", fileContents) .accept("text/plain") .when() .post("/test") diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/MultipartFilenameTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/MultipartFilenameTest.java new file mode 100644 index 0000000000000..89ca96c51be37 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/MultipartFilenameTest.java @@ -0,0 +1,80 @@ +package io.quarkus.rest.client.reactive; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.net.URI; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.jboss.resteasy.reactive.MultipartForm; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.multipart.FileUpload; +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.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class MultipartFilenameTest { + + @TestHTTPResource + URI baseUri; + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class, FormData.class, Client.class, ClientForm.class)) + .withConfigurationResource("dependent-test-application.properties"); + + @Test + void shouldPassOriginalFileName() throws IOException { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + File file = File.createTempFile("MultipartTest", ".txt"); + file.deleteOnExit(); + + ClientForm form = new ClientForm(); + form.file = file; + assertThat(client.postMultipart(form)).isEqualTo(file.getName()); + } + + @Path("/multipart") + @ApplicationScoped + public static class Resource { + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String upload(@MultipartForm FormData form) { + return form.myFile.fileName(); + } + } + + public static class FormData { + @FormParam("myFile") + public FileUpload myFile; + + } + + @Path("/multipart") + public interface Client { + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + String postMultipart(@MultipartForm ClientForm clientForm); + + } + + public static class ClientForm { + @FormParam("myFile") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public File file; + } +} 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 490927bc4c653..f2e1825b9007d 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 @@ -96,6 +96,7 @@ public class SchedulerProcessor { static final DotName SCHEDULED_NAME = DotName.createSimple(Scheduled.class.getName()); static final DotName SCHEDULES_NAME = DotName.createSimple(Scheduled.Schedules.class.getName()); static final DotName SKIP_NEVER_NAME = DotName.createSimple(Scheduled.Never.class.getName()); + static final DotName SKIP_PREDICATE = DotName.createSimple(Scheduled.SkipPredicate.class.getName()); static final Type SCHEDULED_EXECUTION_TYPE = Type.create(DotName.createSimple(ScheduledExecution.class.getName()), Kind.CLASS); @@ -466,4 +467,9 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu return null; } + @BuildStep + UnremovableBeanBuildItem unremoveableSkipPredicates() { + return new UnremovableBeanBuildItem(new UnremovableBeanBuildItem.BeanTypeExclusion(SKIP_PREDICATE)); + } + } diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ConditionalExecutionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ConditionalExecutionTest.java index 6f7e0b9eab131..f04cd3eff6a4b 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ConditionalExecutionTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ConditionalExecutionTest.java @@ -1,11 +1,13 @@ package io.quarkus.scheduler.test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import javax.enterprise.event.Observes; import javax.inject.Singleton; @@ -44,17 +46,25 @@ public void testExecution() { Thread.currentThread().interrupt(); throw new IllegalStateException(e); } + + assertTrue(OtherIsDisabled.TESTED.get()); + assertEquals(0, Jobs.OTHER_COUNT.get()); } static class Jobs { static final CountDownLatch COUNTER = new CountDownLatch(1); + static final AtomicInteger OTHER_COUNT = new AtomicInteger(0); @Scheduled(identity = "foo", every = "1s", skipExecutionIf = IsDisabled.class) void doSomething() throws InterruptedException { COUNTER.countDown(); } + @Scheduled(identity = "other-foo", every = "1s", skipExecutionIf = OtherIsDisabled.class) + void doSomethingElse() throws InterruptedException { + OTHER_COUNT.incrementAndGet(); + } } @Singleton @@ -77,4 +87,17 @@ void onSkip(@Observes SkippedExecution event) { } } + + @Singleton + public static class OtherIsDisabled implements Scheduled.SkipPredicate { + + static final AtomicBoolean TESTED = new AtomicBoolean(false); + + @Override + public boolean test(ScheduledExecution execution) { + TESTED.set(true); + return true; + } + + } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/AuthenticatedCheck.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/AuthenticatedCheck.java index 4b23692988914..ac80cbf0249d9 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/AuthenticatedCheck.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/AuthenticatedCheck.java @@ -2,21 +2,47 @@ import java.lang.reflect.Method; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InstanceHandle; import io.quarkus.security.UnauthorizedException; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.spi.runtime.AuthorizationController; import io.quarkus.security.spi.runtime.SecurityCheck; public class AuthenticatedCheck implements SecurityCheck { public static final AuthenticatedCheck INSTANCE = new AuthenticatedCheck(); + private volatile AuthorizationController authorizationController; + private AuthenticatedCheck() { } @Override public void apply(SecurityIdentity identity, Method method, Object[] parameters) { + if (isAuthorizationDisabled()) { + return; + } if (identity.isAnonymous()) { throw new UnauthorizedException(); } } + + private boolean isAuthorizationDisabled() { + if (authorizationController != null) { + return !authorizationController.isAuthorizationEnabled(); + } + + ArcContainer container = Arc.container(); + if ((container == null) || !container.isRunning()) { + return false; + } + InstanceHandle instance = container.instance(AuthorizationController.class); + if (instance.isAvailable()) { + authorizationController = instance.get(); + return !instance.get().isAuthorizationEnabled(); + } + return false; + } } diff --git a/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfigurationMergerBean.java b/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfigurationMergerBean.java index b14d2983bc62a..20eb364655d00 100644 --- a/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfigurationMergerBean.java +++ b/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfigurationMergerBean.java @@ -13,14 +13,13 @@ * On startup, this beans takes Quarkus-specific configuration of GraphQL clients (quarkus.* properties) * and merges this configuration with the configuration parsed by SmallRye GraphQL itself (CLIENT/mp-graphql/* properties) * - * The resulting merged configuration resides in the application-scoped `io.smallrye.graphql.client.GraphQLClientConfiguration` + * The resulting merged configuration resides in `io.smallrye.graphql.client.GraphQLClientsConfiguration` * * Quarkus configuration overrides SmallRye configuration where applicable. */ @Singleton public class GraphQLClientConfigurationMergerBean { - @Inject GraphQLClientsConfiguration upstreamConfiguration; @Inject @@ -31,6 +30,7 @@ public class GraphQLClientConfigurationMergerBean { @PostConstruct void enhanceGraphQLConfiguration() { + upstreamConfiguration = GraphQLClientsConfiguration.getInstance(); for (Map.Entry client : quarkusConfiguration.clients.entrySet()) { // the raw config key provided in the config, this might be a short class name, // so translate that into the fully qualified name if applicable @@ -43,14 +43,14 @@ void enhanceGraphQLConfiguration() { GraphQLClientConfig quarkusConfig = client.getValue(); // if SmallRye configuration does not contain this client, simply use it - if (!upstreamConfiguration.getClients().containsKey(configKey)) { + if (upstreamConfiguration.getClient(configKey) == null) { GraphQLClientConfiguration transformed = new GraphQLClientConfiguration(); transformed.setHeaders(quarkusConfig.headers); quarkusConfig.url.ifPresent(transformed::setUrl); - upstreamConfiguration.getClients().put(configKey, transformed); + upstreamConfiguration.addClient(configKey, transformed); } else { // if SmallRye configuration already contains this client, override it with the Quarkus configuration - GraphQLClientConfiguration upstreamConfig = upstreamConfiguration.getClients().get(configKey); + GraphQLClientConfiguration upstreamConfig = upstreamConfiguration.getClient(configKey); quarkusConfig.url.ifPresent(upstreamConfig::setUrl); // merge the headers if (quarkusConfig.headers != null) { diff --git a/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/SmallRyeGraphQLClientRecorder.java b/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/SmallRyeGraphQLClientRecorder.java index 041c43129fd9c..a7bffb28e752a 100644 --- a/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/SmallRyeGraphQLClientRecorder.java +++ b/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/SmallRyeGraphQLClientRecorder.java @@ -22,7 +22,8 @@ public Supplier typesafeClientSupplier(Class targetClassName) { } public void setTypesafeApiClasses(List apiClassNames) { - GraphQLClientsConfiguration configBean = Arc.container().instance(GraphQLClientsConfiguration.class).get(); + GraphQLClientsConfiguration.setSingleApplication(true); + GraphQLClientsConfiguration configBean = GraphQLClientsConfiguration.getInstance(); List> classes = apiClassNames.stream().map(className -> { try { return Class.forName(className, true, Thread.currentThread().getContextClassLoader()); @@ -30,7 +31,7 @@ public void setTypesafeApiClasses(List apiClassNames) { throw new RuntimeException(e); } }).collect(Collectors.toList()); - configBean.apiClasses(classes, true); + configBean.addTypesafeClientApis(classes); } public RuntimeValue clientSupport(Map shortNamesToQualifiedNames) { diff --git a/extensions/smallrye-reactive-messaging-amqp/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/smallrye-reactive-messaging-amqp/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 17e0d2833eb84..ad13261008daa 100644 --- a/extensions/smallrye-reactive-messaging-amqp/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/smallrye-reactive-messaging-amqp/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,4 +9,4 @@ metadata: guide: "https://quarkus.io/guides/amqp" categories: - "messaging" - status: "preview" \ No newline at end of file + status: "stable" 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 aa6532a2f53f3..a04122a1ce38d 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 @@ -532,7 +532,11 @@ public void run() { } public void addServletContextAttribute(RuntimeValue deployment, String key, Object value1) { - deployment.getValue().addServletContextAttribute(key, value1); + if (value1 instanceof RuntimeValue) { + deployment.getValue().addServletContextAttribute(key, ((RuntimeValue) value1).getValue()); + } else { + deployment.getValue().addServletContextAttribute(key, value1); + } } public void addServletExtension(RuntimeValue deployment, ServletExtension extension) { diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketListener.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketTestListener.java similarity index 56% rename from extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketListener.java rename to extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketTestListener.java index 703dfe9514f44..2d83492a8186b 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketListener.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketTestListener.java @@ -11,7 +11,9 @@ import io.quarkus.deployment.dev.testing.TestRunResults; import io.quarkus.dev.testing.ContinuousTestingWebsocketListener; -public class ContinuousTestingWebSocketListener implements TestListener { +public class ContinuousTestingWebSocketTestListener implements TestListener { + + private volatile ContinuousTestingWebsocketListener.State lastState; @Override public void listenerRegistered(TestController testController) { @@ -20,9 +22,18 @@ public void listenerRegistered(TestController testController) { @Override public void testsEnabled() { - ContinuousTestingWebsocketListener - .setLastState( - new ContinuousTestingWebsocketListener.State(true, true, 0L, 0L, 0L, 0L, false, false, false, true)); + if (lastState == null) { + ContinuousTestingWebsocketListener + .setLastState(new ContinuousTestingWebsocketListener.State(true, false, + 0, 0, 0, 0, + ContinuousTestingWebsocketListener.getLastState().isBrokenOnly, + ContinuousTestingWebsocketListener.getLastState().isTestOutput, + ContinuousTestingWebsocketListener.getLastState().isInstrumentationBasedReload, + ContinuousTestingWebsocketListener.getLastState().isLiveReload)); + } else { + ContinuousTestingWebsocketListener + .setLastState(lastState); + } } @Override @@ -46,17 +57,18 @@ public void testComplete(TestResult result) { @Override public void runComplete(TestRunResults testRunResults) { + lastState = new ContinuousTestingWebsocketListener.State(true, false, + testRunResults.getPassedCount() + + testRunResults.getFailedCount() + + testRunResults.getSkippedCount(), + testRunResults.getPassedCount(), + testRunResults.getFailedCount(), testRunResults.getSkippedCount(), + ContinuousTestingWebsocketListener.getLastState().isBrokenOnly, + ContinuousTestingWebsocketListener.getLastState().isTestOutput, + ContinuousTestingWebsocketListener.getLastState().isInstrumentationBasedReload, + ContinuousTestingWebsocketListener.getLastState().isLiveReload); ContinuousTestingWebsocketListener.setLastState( - new ContinuousTestingWebsocketListener.State(true, false, - testRunResults.getPassedCount() + - testRunResults.getFailedCount() + - testRunResults.getSkippedCount(), - testRunResults.getPassedCount(), - testRunResults.getFailedCount(), testRunResults.getSkippedCount(), - ContinuousTestingWebsocketListener.getLastState().isBrokenOnly, - ContinuousTestingWebsocketListener.getLastState().isTestOutput, - ContinuousTestingWebsocketListener.getLastState().isInstrumentationBasedReload, - ContinuousTestingWebsocketListener.getLastState().isLiveReload)); + lastState); } @Override diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java index fe0a98105eb8b..6bc93507d3b80 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java @@ -23,7 +23,7 @@ import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; -import io.quarkus.vertx.http.deployment.devmode.console.ContinuousTestingWebSocketListener; +import io.quarkus.vertx.http.deployment.devmode.console.ContinuousTestingWebSocketTestListener; import io.quarkus.vertx.http.runtime.devmode.DevConsoleRecorder; import io.quarkus.vertx.http.runtime.devmode.Json; import io.vertx.core.Handler; @@ -61,7 +61,7 @@ public void setupTestRoutes( .route("dev/test") .handler(recorder.continousTestHandler(shutdownContextBuildItem)) .build()); - testListenerBuildItemBuildProducer.produce(new TestListenerBuildItem(new ContinuousTestingWebSocketListener())); + testListenerBuildItemBuildProducer.produce(new TestListenerBuildItem(new ContinuousTestingWebSocketTestListener())); } } diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/tests.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/tests.html index f5e8c77ca9141..bf9981b60745f 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/tests.html +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/tests.html @@ -87,7 +87,7 @@


-
+
{#for throwable in r.problems}
{throwable.message}
@@ -100,7 +100,7 @@
{#if !r.logOutput.empty}
-
+
{#for log in r.logOutput} {log.raw}
@@ -124,7 +124,7 @@
{#if !r.logOutput.empty}
-
+
{#for log in r.logOutput} {log.raw}
@@ -149,7 +149,7 @@
{#if !r.logOutput.empty}
-
+
{#for log in r.logOutput} {log.raw}
@@ -191,7 +191,7 @@

-
+
{#for log in r.logOutput} {log.raw}
@@ -214,7 +214,7 @@

-
+
{#for log in r.logOutput} {log.raw}
@@ -256,7 +256,7 @@

-
+
{#for log in r.logOutput} {log.raw}
diff --git a/extensions/vertx-http/runtime/pom.xml b/extensions/vertx-http/runtime/pom.xml index a1f7c667077d8..fabcf3c34282d 100644 --- a/extensions/vertx-http/runtime/pom.xml +++ b/extensions/vertx-http/runtime/pom.xml @@ -53,6 +53,11 @@ + + org.graalvm.nativeimage + svm + provided + org.junit.jupiter diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/graal/HttpContentCompressorSubstitutions.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/graal/HttpContentCompressorSubstitutions.java new file mode 100644 index 0000000000000..37dac442fbcf1 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/graal/HttpContentCompressorSubstitutions.java @@ -0,0 +1,115 @@ +package io.quarkus.vertx.http.runtime.graal; + +import static io.netty.handler.codec.http.HttpHeaderValues.DEFLATE; +import static io.netty.handler.codec.http.HttpHeaderValues.GZIP; +import static io.netty.handler.codec.http.HttpHeaderValues.X_DEFLATE; +import static io.netty.handler.codec.http.HttpHeaderValues.X_GZIP; + +import java.util.function.BooleanSupplier; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.compression.ZlibWrapper; +import io.netty.handler.codec.http2.CompressorHttp2ConnectionEncoder; +import io.netty.handler.codec.http2.Http2Exception; + +public class HttpContentCompressorSubstitutions { + + @TargetClass(className = "io.netty.handler.codec.compression.ZstdEncoder", onlyWith = IsZstdAbsent.class) + public static final class ZstdEncoderFactorySubstitution { + + @Substitute + protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception { + throw new UnsupportedOperationException(); + } + + @Substitute + protected void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) { + throw new UnsupportedOperationException(); + } + + @Substitute + public void flush(final ChannelHandlerContext ctx) { + throw new UnsupportedOperationException(); + } + } + + @TargetClass(className = "io.netty.handler.codec.compression.BrotliEncoder", onlyWith = IsBrotliAbsent.class) + public static final class BrEncoderFactorySubstitution { + + @Substitute + protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception { + throw new UnsupportedOperationException(); + } + + @Substitute + protected void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) { + throw new UnsupportedOperationException(); + } + } + + @TargetClass(CompressorHttp2ConnectionEncoder.class) + public static final class CompressorHttp2ConnectionSubstitute { + + @Substitute + protected EmbeddedChannel newContentCompressor(ChannelHandlerContext ctx, CharSequence contentEncoding) + throws Http2Exception { + if (GZIP.contentEqualsIgnoreCase(contentEncoding) || X_GZIP.contentEqualsIgnoreCase(contentEncoding)) { + return newCompressionChannel(ctx, ZlibWrapper.GZIP); + } + if (DEFLATE.contentEqualsIgnoreCase(contentEncoding) || X_DEFLATE.contentEqualsIgnoreCase(contentEncoding)) { + return newCompressionChannel(ctx, ZlibWrapper.ZLIB); + } + // 'identity' or unsupported + return null; + } + + @Alias + private EmbeddedChannel newCompressionChannel(final ChannelHandlerContext ctx, ZlibWrapper wrapper) { + throw new UnsupportedOperationException(); + } + } + + public static class IsZstdAbsent implements BooleanSupplier { + + private boolean zstdAbsent; + + public IsZstdAbsent() { + try { + Class.forName("com.github.luben.zstd.Zstd"); + zstdAbsent = false; + } catch (ClassNotFoundException e) { + zstdAbsent = true; + } + } + + @Override + public boolean getAsBoolean() { + return zstdAbsent; + } + } + + public static class IsBrotliAbsent implements BooleanSupplier { + + private boolean brotliAbsent; + + public IsBrotliAbsent() { + try { + Class.forName("com.aayushatharva.brotli4j.encoder.Encoder"); + brotliAbsent = false; + } catch (ClassNotFoundException e) { + brotliAbsent = true; + } + } + + @Override + public boolean getAsBoolean() { + return brotliAbsent; + } + } +} diff --git a/extensions/websockets/client/deployment/pom.xml b/extensions/websockets/client/deployment/pom.xml index 079ab2aa0e009..0fa240617ff7e 100644 --- a/extensions/websockets/client/deployment/pom.xml +++ b/extensions/websockets/client/deployment/pom.xml @@ -21,6 +21,10 @@ io.quarkus quarkus-websockets-client + + io.quarkus + quarkus-undertow-spi + io.quarkus quarkus-junit5-internal diff --git a/extensions/websockets/client/deployment/src/main/java/io/quarkus/undertow/websockets/client/deployment/WebsocketClientProcessor.java b/extensions/websockets/client/deployment/src/main/java/io/quarkus/undertow/websockets/client/deployment/WebsocketClientProcessor.java index 44574779a8e86..c38f426a850e0 100644 --- a/extensions/websockets/client/deployment/src/main/java/io/quarkus/undertow/websockets/client/deployment/WebsocketClientProcessor.java +++ b/extensions/websockets/client/deployment/src/main/java/io/quarkus/undertow/websockets/client/deployment/WebsocketClientProcessor.java @@ -12,6 +12,7 @@ import javax.websocket.ContainerProvider; import javax.websocket.Endpoint; import javax.websocket.server.ServerApplicationConfig; +import javax.websocket.server.ServerContainer; import javax.websocket.server.ServerEndpointConfig; import org.jboss.jandex.AnnotationInstance; @@ -34,10 +35,11 @@ import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; -import io.quarkus.netty.deployment.EventLoopSupplierBuildItem; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.undertow.deployment.ServletContextAttributeBuildItem; import io.quarkus.undertow.websockets.client.runtime.WebsocketCoreRecorder; import io.undertow.websockets.DefaultContainerConfigurator; +import io.undertow.websockets.ServerWebSocketContainer; import io.undertow.websockets.UndertowContainerProvider; import io.undertow.websockets.WebSocketDeploymentInfo; @@ -70,16 +72,16 @@ void scanForAnnotatedEndpoints(CombinedIndexBuildItem indexBuildItem, } @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) + @Record(ExecutionTime.STATIC_INIT) public ServerWebSocketContainerBuildItem deploy(final CombinedIndexBuildItem indexBuildItem, WebsocketCoreRecorder recorder, BuildProducer reflection, - EventLoopSupplierBuildItem eventLoopSupplierBuildItem, List annotatedEndpoints, BeanContainerBuildItem beanContainerBuildItem, WebsocketConfig websocketConfig, BuildProducer infoBuildItemBuildProducer, - Optional factoryBuildItem) throws Exception { + Optional factoryBuildItem, + BuildProducer servletContextAttributeBuildItemBuildProducer) throws Exception { final Set endpoints = new HashSet<>(); final Set config = new HashSet<>(); @@ -121,10 +123,14 @@ public ServerWebSocketContainerBuildItem deploy(final CombinedIndexBuildItem ind websocketConfig.maxFrameSize, websocketConfig.dispatchToWorker); infoBuildItemBuildProducer.produce(new WebSocketDeploymentInfoBuildItem(deploymentInfo)); + RuntimeValue serverContainer = recorder.createServerContainer( + beanContainerBuildItem.getValue(), + deploymentInfo, + factoryBuildItem.map(ServerWebSocketContainerFactoryBuildItem::getFactory).orElse(null)); + servletContextAttributeBuildItemBuildProducer + .produce(new ServletContextAttributeBuildItem(ServerContainer.class.getName(), serverContainer)); return new ServerWebSocketContainerBuildItem( - recorder.createServerContainer(beanContainerBuildItem.getValue(), eventLoopSupplierBuildItem.getMainSupplier(), - deploymentInfo, - factoryBuildItem.map(ServerWebSocketContainerFactoryBuildItem::getFactory).orElse(null))); + serverContainer); } public static void registerCodersForReflection(BuildProducer reflection, diff --git a/extensions/websockets/client/runtime/src/main/java/io/quarkus/undertow/websockets/client/runtime/WebsocketCoreRecorder.java b/extensions/websockets/client/runtime/src/main/java/io/quarkus/undertow/websockets/client/runtime/WebsocketCoreRecorder.java index 1c384fc46a3e5..1a420478fefb4 100644 --- a/extensions/websockets/client/runtime/src/main/java/io/quarkus/undertow/websockets/client/runtime/WebsocketCoreRecorder.java +++ b/extensions/websockets/client/runtime/src/main/java/io/quarkus/undertow/websockets/client/runtime/WebsocketCoreRecorder.java @@ -19,6 +19,7 @@ import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.vertx.core.runtime.VertxCoreRecorder; import io.undertow.websockets.ServerWebSocketContainer; import io.undertow.websockets.UndertowContainerProvider; import io.undertow.websockets.WebSocketDeploymentInfo; @@ -26,6 +27,7 @@ import io.undertow.websockets.util.ObjectFactory; import io.undertow.websockets.util.ObjectHandle; import io.undertow.websockets.util.ObjectIntrospecter; +import io.vertx.core.impl.VertxInternal; @Recorder public class WebsocketCoreRecorder { @@ -105,7 +107,6 @@ public RuntimeValue createDeploymentInfo(Set an } public RuntimeValue createServerContainer(BeanContainer beanContainer, - Supplier eventLoopGroupSupplier, RuntimeValue infoVal, ServerWebSocketContainerFactory serverContainerFactory) throws DeploymentException { WebSocketDeploymentInfo info = infoVal.getValue(); @@ -136,7 +137,12 @@ public void release() { } }; } - }, Thread.currentThread().getContextClassLoader(), eventLoopGroupSupplier, + }, Thread.currentThread().getContextClassLoader(), new Supplier() { + @Override + public EventLoopGroup get() { + return ((VertxInternal) VertxCoreRecorder.getVertx().get()).getEventLoopGroup(); + } + }, Collections.singletonList(new ContextSetupHandler() { @Override public Action create(Action action) { diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index 517018911779c..851d59e5ea558 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -177,7 +177,7 @@ public abstract class EndpointIndexer> classLevelExceptionMappers; private final Function> factoryCreator; - private final Consumer> resourceMethodCallback; + private final Consumer resourceMethodCallback; protected EndpointIndexer(Builder builder) { this.index = builder.index; @@ -545,7 +545,7 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf handleAdditionalMethodProcessing((METHOD) method, currentClassInfo, currentMethodInfo); if (resourceMethodCallback != null) { - resourceMethodCallback.accept(new AbstractMap.SimpleEntry<>(currentMethodInfo, method)); + resourceMethodCallback.accept(new ResourceMethodCallbackData(currentMethodInfo, actualEndpointInfo, method)); } return method; } catch (Exception e) { @@ -1133,7 +1133,7 @@ public static abstract class Builder, B private AdditionalWriters additionalWriters; private boolean hasRuntimeConverters; private Map> classLevelExceptionMappers; - private Consumer> resourceMethodCallback; + private Consumer resourceMethodCallback; public B setDefaultBlocking(BlockingDefault defaultBlocking) { this.defaultBlocking = defaultBlocking; @@ -1195,7 +1195,7 @@ public B setClassLevelExceptionMappers(Map> classLe return (B) this; } - public B setResourceMethodCallback(Consumer> resourceMethodCallback) { + public B setResourceMethodCallback(Consumer resourceMethodCallback) { this.resourceMethodCallback = resourceMethodCallback; return (B) this; } @@ -1203,4 +1203,28 @@ public B setResourceMethodCallback(Consumer1.1.6 1.0.0 1.6.0 - 4.1.2 + 4.1.3 4.4.0 1.0.0.Final 2.0.0.Final diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/CacheControlScanner.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/CacheControlScanner.java index 5b5ef46790639..6464523f41e22 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/CacheControlScanner.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/CacheControlScanner.java @@ -61,18 +61,18 @@ private ExtendedCacheControl noCacheToCacheControl(AnnotationInstance noCacheIns if (noCacheInstance == null) { return null; } + ExtendedCacheControl cacheControl = new ExtendedCacheControl(); + cacheControl.setNoCache(true); + cacheControl.setNoTransform(false); AnnotationValue fieldsValue = noCacheInstance.value("fields"); if (fieldsValue != null) { String[] fields = fieldsValue.asStringArray(); if ((fields != null) && (fields.length > 0)) { - ExtendedCacheControl cacheControl = new ExtendedCacheControl(); - cacheControl.setNoCache(true); - cacheControl.setNoTransform(false); cacheControl.getNoCacheFields().addAll(Arrays.asList(fields)); - return cacheControl; + } } - return null; + return cacheControl; } private ExtendedCacheControl cacheToCacheControl(AnnotationInstance cacheInstance) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/injection/ContextProducers.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/injection/ContextProducers.java index 06318c42c1c2d..74c67f3822420 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/injection/ContextProducers.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/injection/ContextProducers.java @@ -133,6 +133,10 @@ SecurityContext securityContext() { } private ResteasyReactiveRequestContext getContext() { - return CurrentRequestManager.get(); + ResteasyReactiveRequestContext context = CurrentRequestManager.get(); + if (context == null) { + throw new IllegalStateException("No RESTEasy Reactive request in progress"); + } + return context; } } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java index d33429cbfeb49..d06ba6177c47b 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java @@ -189,7 +189,8 @@ private List computeRequiredExtensions(ExtensionCatalog catalog, return extensionsToAdd; } - private List getExtensionOrigins(ExtensionCatalog extensionCatalog, List extensionsToAdd) + private static List getExtensionOrigins(ExtensionCatalog extensionCatalog, + List extensionsToAdd) throws QuarkusCommandException { final List extOrigins = new ArrayList<>(extensionsToAdd.size()); @@ -227,7 +228,7 @@ private List getExtensionOrigins(ExtensionCatalog extensionCat return recommendedCombination.getUniqueSortedOrigins().stream().map(o -> o.getCatalog()).collect(Collectors.toList()); } - public void addOrigins(final List extOrigins, Extension e) { + private static void addOrigins(final List extOrigins, Extension e) { ExtensionOrigins.Builder eoBuilder = null; for (ExtensionOrigin o : e.getOrigins()) { if (!(o instanceof ExtensionCatalog)) { diff --git a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClient.java b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClient.java index 70f6a22777edd..97f532d2a3638 100644 --- a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClient.java +++ b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClient.java @@ -120,6 +120,9 @@ private ExtensionCatalog deserializeExtensionCatalog(Path p) throws RegistryReso public PlatformCatalog resolvePlatforms(String quarkusVersion) throws RegistryResolutionException { final Path json = TestRegistryClientBuilder.getRegistryPlatformsCatalogPath(registryDir, quarkusVersion); log.debug("%s resolvePlatforms %s", config.getId(), json); + if (!Files.exists(json)) { + return null; + } try { return JsonCatalogMapperHelper.deserialize(json, JsonPlatformCatalog.class); } catch (IOException e) { diff --git a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClientBuilder.java b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClientBuilder.java index 74936a9ea295a..195a7c1103ee4 100644 --- a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClientBuilder.java +++ b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClientBuilder.java @@ -106,6 +106,7 @@ public static class TestRegistryBuilder { private JsonRegistryQuarkusVersionsConfig quarkusVersions; private boolean external; private PlatformCatalog platformCatalog; + private JsonPlatformCatalog archivedPlatformCatalog; private List memberCatalogs; private List nonPlatformCatalogs; @@ -221,37 +222,72 @@ private void configure(Path registryDir) { platformConfig .setArtifact(new ArtifactCoords(registryGroupId, Constants.DEFAULT_REGISTRY_PLATFORMS_CATALOG_ARTIFACT_ID, null, "json", Constants.DEFAULT_REGISTRY_ARTIFACT_VERSION)); - if (platformCatalog == null) { + if (platformCatalog == null && archivedPlatformCatalog == null) { platformConfig.setDisabled(true); } else { final Path platformsDir = getRegistryPlatformsDir(registryDir); - persistPlatformCatalog(platformCatalog, platformsDir); final Map platformsByQuarkusVersion = new HashMap<>(); - for (Platform p : platformCatalog.getPlatforms()) { - for (PlatformStream s : p.getStreams()) { - for (PlatformRelease r : s.getReleases()) { - if (r.getQuarkusCoreVersion() == null) { - throw new IllegalStateException("Quarkus version has not be configured for platform release " - + p.getPlatformKey() + ":" + s.getId() + ":" + r.getVersion()); - } - final JsonPlatformCatalog c = platformsByQuarkusVersion.computeIfAbsent(r.getQuarkusCoreVersion(), - v -> new JsonPlatformCatalog()); - JsonPlatform platform = (JsonPlatform) c.getPlatform(p.getPlatformKey()); - if (platform == null) { - platform = new JsonPlatform(); - platform.setPlatformKey(p.getPlatformKey()); - c.addPlatform(platform); + if (platformCatalog != null) { + persistPlatformCatalog(platformCatalog, platformsDir); + for (Platform p : platformCatalog.getPlatforms()) { + for (PlatformStream s : p.getStreams()) { + for (PlatformRelease r : s.getReleases()) { + if (r.getQuarkusCoreVersion() == null) { + throw new IllegalStateException( + "Quarkus version has not be configured for platform release " + + p.getPlatformKey() + ":" + s.getId() + ":" + r.getVersion()); + } + final JsonPlatformCatalog c = platformsByQuarkusVersion.computeIfAbsent( + r.getQuarkusCoreVersion(), + v -> new JsonPlatformCatalog()); + JsonPlatform platform = (JsonPlatform) c.getPlatform(p.getPlatformKey()); + if (platform == null) { + platform = new JsonPlatform(); + platform.setPlatformKey(p.getPlatformKey()); + c.addPlatform(platform); + } + JsonPlatformStream stream = (JsonPlatformStream) platform.getStream(s.getId()); + if (stream == null) { + stream = new JsonPlatformStream(); + stream.setId(s.getId()); + platform.addStream(stream); + } + stream.addRelease(r); } - JsonPlatformStream stream = (JsonPlatformStream) platform.getStream(s.getId()); - if (stream == null) { - stream = new JsonPlatformStream(); - stream.setId(s.getId()); - platform.addStream(stream); + } + } + } + + if (archivedPlatformCatalog != null) { + for (Platform p : archivedPlatformCatalog.getPlatforms()) { + for (PlatformStream s : p.getStreams()) { + for (PlatformRelease r : s.getReleases()) { + if (r.getQuarkusCoreVersion() == null) { + throw new IllegalStateException( + "Quarkus version has not be configured for platform release " + + p.getPlatformKey() + ":" + s.getId() + ":" + r.getVersion()); + } + final JsonPlatformCatalog c = platformsByQuarkusVersion.computeIfAbsent( + r.getQuarkusCoreVersion(), + v -> new JsonPlatformCatalog()); + JsonPlatform platform = (JsonPlatform) c.getPlatform(p.getPlatformKey()); + if (platform == null) { + platform = new JsonPlatform(); + platform.setPlatformKey(p.getPlatformKey()); + c.addPlatform(platform); + } + JsonPlatformStream stream = (JsonPlatformStream) platform.getStream(s.getId()); + if (stream == null) { + stream = new JsonPlatformStream(); + stream.setId(s.getId()); + platform.addStream(stream); + } + stream.addRelease(r); } - stream.addRelease(r); } } } + for (Map.Entry entry : platformsByQuarkusVersion.entrySet()) { persistPlatformCatalog(entry.getValue(), platformsDir.resolve(entry.getKey())); } @@ -339,6 +375,33 @@ public TestPlatformCatalogReleaseBuilder newRelease(String version) { return new TestPlatformCatalogReleaseBuilder(this, release); } + public TestPlatformCatalogReleaseBuilder newArchivedRelease(String version) { + final JsonPlatformRelease release = new JsonPlatformRelease(); + release.setVersion(JsonPlatformReleaseVersion.fromString(version)); + + if (platform.registry.archivedPlatformCatalog == null) { + platform.registry.archivedPlatformCatalog = new JsonPlatformCatalog(); + } + + JsonPlatform archivedPlatform = (JsonPlatform) platform.registry.archivedPlatformCatalog + .getPlatform(platform.platform.getPlatformKey()); + if (archivedPlatform == null) { + archivedPlatform = new JsonPlatform(); + archivedPlatform.setPlatformKey(platform.platform.getPlatformKey()); + platform.registry.archivedPlatformCatalog.addPlatform(archivedPlatform); + } + + JsonPlatformStream archivedStream = (JsonPlatformStream) archivedPlatform.getStream(stream.getId()); + if (archivedStream == null) { + archivedStream = new JsonPlatformStream(); + archivedStream.setId(stream.getId()); + archivedPlatform.addStream(archivedStream); + } + + archivedStream.addRelease(release); + return new TestPlatformCatalogReleaseBuilder(this, release); + } + public TestPlatformCatalogPlatformBuilder platform() { return platform; } diff --git a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/project/create/MultiplePlatformBomsTestBase.java b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/project/create/MultiplePlatformBomsTestBase.java index d38ae9eb04136..59c58b2007a9d 100644 --- a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/project/create/MultiplePlatformBomsTestBase.java +++ b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/project/create/MultiplePlatformBomsTestBase.java @@ -13,6 +13,8 @@ import io.quarkus.devtools.project.QuarkusProjectHelper; import io.quarkus.devtools.testing.registry.client.TestRegistryClientBuilder; import io.quarkus.maven.ArtifactCoords; +import io.quarkus.registry.RegistryResolutionException; +import io.quarkus.registry.catalog.PlatformStreamCoords; import io.quarkus.registry.config.RegistriesConfigLocator; import java.io.IOException; import java.nio.file.Files; @@ -89,7 +91,7 @@ protected QuarkusCommandOutcome addExtensions(Path projectDir, List exte protected QuarkusCommandOutcome createProject(Path projectDir, List extensions) throws Exception { - return createProject(projectDir, null, extensions); + return createProject(projectDir, (String) null, extensions); } protected QuarkusCommandOutcome createProject(Path projectDir, String quarkusVersion, List extensions) @@ -103,6 +105,17 @@ protected QuarkusCommandOutcome createProject(Path projectDir, String quarkusVer .execute(); } + protected QuarkusCommandOutcome createProject(Path projectDir, PlatformStreamCoords stream, List extensions) + throws Exception { + return new CreateProject( + getQuarkusProject(projectDir, stream)) + .groupId("org.acme") + .artifactId("acme-app") + .version("0.0.1-SNAPSHOT") + .extensions(new HashSet<>(extensions)) + .execute(); + } + protected List toPlatformExtensionCoords(String... artifactIds) { return toPlatformExtensionCoords(Arrays.asList(artifactIds)); } @@ -171,6 +184,12 @@ protected QuarkusProject getQuarkusProject(Path projectDir, String quarkusVersio return QuarkusProjectHelper.getProject(projectDir, BuildTool.MAVEN, quarkusVersion); } + protected QuarkusProject getQuarkusProject(Path projectDir, PlatformStreamCoords stream) + throws RegistryResolutionException { + return QuarkusProjectHelper.getProject(projectDir, + QuarkusProjectHelper.getCatalogResolver().resolveExtensionCatalog(stream), BuildTool.MAVEN); + } + static Path newProjectDir(String name) { Path projectDir = getProjectDir(name); if (Files.exists(projectDir)) { diff --git a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/project/create/QuarkusPlatformReferencingArchivedUpstreamVersionTest.java b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/project/create/QuarkusPlatformReferencingArchivedUpstreamVersionTest.java new file mode 100644 index 0000000000000..289b13db42130 --- /dev/null +++ b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/project/create/QuarkusPlatformReferencingArchivedUpstreamVersionTest.java @@ -0,0 +1,162 @@ +package io.quarkus.devtools.project.create; + +import io.quarkus.devtools.testing.registry.client.TestRegistryClientBuilder; +import io.quarkus.maven.ArtifactCoords; +import io.quarkus.registry.catalog.PlatformStreamCoords; +import java.nio.file.Path; +import java.util.Arrays; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class QuarkusPlatformReferencingArchivedUpstreamVersionTest extends MultiplePlatformBomsTestBase { + + private static final String DOWNSTREAM_PLATFORM_KEY = "io.downstream.platform"; + private static final String UPSTREAM_PLATFORM_KEY = "io.upstream.platform"; + + @BeforeAll + public static void setup() throws Exception { + TestRegistryClientBuilder.newInstance() + //.debug() + .baseDir(configDir()) + // registry + .newRegistry("downstream.registry.test") + .recognizedQuarkusVersions("*-downstream") + // platform key + .newPlatform(DOWNSTREAM_PLATFORM_KEY) + // 2.0 STREAM + .newStream("2.0") + // 2.0.4 release + .newRelease("2.0.4-downstream") + .quarkusVersion("2.2.2-downstream") + .upstreamQuarkusVersion("2.2.2") + // default bom including quarkus-core + essential metadata + .addCoreMember() + // foo platform member + .newMember("acme-foo-bom").addExtension("io.acme", "ext-a", "2.0.4-downstream").release().stream().platform() + .newStream("1.0") + // 1.0.4 release + .newRelease("1.0.4-downstream") + .quarkusVersion("1.1.1-downstream") + .upstreamQuarkusVersion("1.1.1") + // default bom including quarkus-core + essential metadata + .addCoreMember() + // foo platform member + .newMember("acme-foo-bom").addExtension("io.acme", "ext-a", "1.0.4-downstream").release() + .newMember("acme-e-bom").addExtension("io.acme", "ext-e", "1.0.4-downstream").release() + .stream().platform().registry() + .newNonPlatformCatalog("1.1.1-downstream").addExtension("io.acme", "ext-d", "4.0-downstream").registry() + .clientBuilder() + .newRegistry("upstream.registry.test") + // platform key + .newPlatform(UPSTREAM_PLATFORM_KEY) + // 2.0 STREAM + .newStream("2.0") + // 2.0.5 release + .newRelease("2.0.5") + .quarkusVersion("2.2.5") + // default bom including quarkus-core + essential metadata + .addCoreMember() + .newMember("acme-foo-bom").addExtension("io.acme", "ext-a", "2.0.5").release() + .newMember("acme-e-bom").addExtension("io.acme", "ext-e", "2.0.5").release() + .newMember("acme-bar-bom").addExtension("io.acme", "ext-b", "2.0.5").release().stream() + // 2.0.4 release + .newArchivedRelease("2.0.4") + .quarkusVersion("2.2.2") + // default bom including quarkus-core + essential metadata + .addCoreMember() + .newMember("acme-foo-bom").addExtension("io.acme", "ext-a", "2.0.4").release() + .newMember("acme-e-bom").addExtension("io.acme", "ext-e", "2.0.4").release() + .newMember("acme-bar-bom").addExtension("io.acme", "ext-b", "2.0.4").release().stream().platform() + // 1.0 STREAM + .newStream("1.0") + .newRelease("1.0.5") + .quarkusVersion("1.1.5") + // default bom including quarkus-core + essential metadata + .addCoreMember() + .newMember("acme-foo-bom").addExtension("io.acme", "ext-a", "1.0.5").addExtension("io.acme", "ext-e", "1.0.5") + .release() + .newMember("acme-bar-bom").addExtension("io.acme", "ext-b", "1.0.5").release() + .stream() + .newArchivedRelease("1.0.4") + .quarkusVersion("1.1.1") + // default bom including quarkus-core + essential metadata + .addCoreMember() + .newMember("acme-foo-bom").addExtension("io.acme", "ext-a", "1.0.4").addExtension("io.acme", "ext-e", "1.0.4") + .release() + .newMember("acme-bar-bom").addExtension("io.acme", "ext-b", "1.0.4").release() + .stream().platform().registry() + .newNonPlatformCatalog("2.2.2").addExtension("io.acme", "ext-c", "5.1").addExtension("io.acme", "ext-d", "6.0") + .registry() + .clientBuilder() + .build(); + + enableRegistryClient(); + } + + protected String getMainPlatformKey() { + return DOWNSTREAM_PLATFORM_KEY; + } + + @Test + public void addExtensionsFromAlreadyImportedPlatform() throws Exception { + final Path projectDir = newProjectDir("downstream-upstream-platform"); + createProject(projectDir, Arrays.asList("ext-a")); + + assertModel(projectDir, + toPlatformBomCoords("acme-foo-bom"), + Arrays.asList(new ArtifactCoords("io.acme", "ext-a", null)), + "2.0.4-downstream"); + + addExtensions(projectDir, Arrays.asList("ext-b", "ext-c", "ext-d", "ext-e")); + assertModel(projectDir, + Arrays.asList(mainPlatformBom(), platformMemberBomCoords("acme-foo-bom"), + new ArtifactCoords(UPSTREAM_PLATFORM_KEY, "acme-bar-bom", "pom", "2.0.4"), + new ArtifactCoords(UPSTREAM_PLATFORM_KEY, "acme-e-bom", "pom", "2.0.4")), + Arrays.asList(new ArtifactCoords("io.acme", "ext-a", null), + new ArtifactCoords("io.acme", "ext-b", null), + new ArtifactCoords("io.acme", "ext-e", null), + new ArtifactCoords("io.acme", "ext-c", "jar", "5.1"), + new ArtifactCoords("io.acme", "ext-d", "jar", "6.0")), + "2.0.4-downstream"); + } + + @Test + public void createWithExtensionsFromDifferentPlatforms() throws Exception { + final Path projectDir = newProjectDir("create-downstream-upstream-platform"); + createProject(projectDir, Arrays.asList("ext-a", "ext-b")); + + assertModel(projectDir, + Arrays.asList(mainPlatformBom(), platformMemberBomCoords("acme-foo-bom"), + new ArtifactCoords(UPSTREAM_PLATFORM_KEY, "acme-bar-bom", "pom", "2.0.4")), + Arrays.asList(new ArtifactCoords("io.acme", "ext-a", null), new ArtifactCoords("io.acme", "ext-b", null)), + "2.0.4-downstream"); + } + + @Test + public void createPreferringOlderStreamToNewerStreamCoveringLessExtensions() throws Exception { + final Path projectDir = newProjectDir("create-downstream-upstream-platform"); + createProject(projectDir, Arrays.asList("ext-a", "ext-b", "ext-e")); + + assertModel(projectDir, + Arrays.asList(mainPlatformBom(), platformMemberBomCoords("acme-foo-bom"), platformMemberBomCoords("acme-e-bom"), + new ArtifactCoords(UPSTREAM_PLATFORM_KEY, "acme-bar-bom", "pom", "1.0.4")), + Arrays.asList(new ArtifactCoords("io.acme", "ext-a", null), new ArtifactCoords("io.acme", "ext-b", null), + new ArtifactCoords("io.acme", "ext-e", null)), + "1.0.4-downstream"); + } + + @Test + public void createUsingStream2_0() throws Exception { + final Path projectDir = newProjectDir("created-using-downstream-stream"); + createProject(projectDir, new PlatformStreamCoords(DOWNSTREAM_PLATFORM_KEY, "2.0"), + Arrays.asList("ext-a", "ext-b", "ext-e")); + + assertModel(projectDir, + Arrays.asList(mainPlatformBom(), platformMemberBomCoords("acme-foo-bom"), + new ArtifactCoords(UPSTREAM_PLATFORM_KEY, "acme-e-bom", "pom", "2.0.4"), + new ArtifactCoords(UPSTREAM_PLATFORM_KEY, "acme-bar-bom", "pom", "2.0.4")), + Arrays.asList(new ArtifactCoords("io.acme", "ext-a", null), new ArtifactCoords("io.acme", "ext-b", null), + new ArtifactCoords("io.acme", "ext-e", null)), + "2.0.4-downstream"); + } +} diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/ExtensionCatalogResolver.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/ExtensionCatalogResolver.java index 3d2059862f146..ec04881f12ed5 100644 --- a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/ExtensionCatalogResolver.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/ExtensionCatalogResolver.java @@ -4,12 +4,12 @@ import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.devtools.messagewriter.MessageWriter; import io.quarkus.maven.ArtifactCoords; -import io.quarkus.maven.StreamCoords; import io.quarkus.registry.catalog.ExtensionCatalog; import io.quarkus.registry.catalog.Platform; import io.quarkus.registry.catalog.PlatformCatalog; import io.quarkus.registry.catalog.PlatformRelease; import io.quarkus.registry.catalog.PlatformStream; +import io.quarkus.registry.catalog.PlatformStreamCoords; import io.quarkus.registry.catalog.json.JsonCatalogMerger; import io.quarkus.registry.catalog.json.JsonExtensionCatalog; import io.quarkus.registry.catalog.json.JsonPlatform; @@ -291,6 +291,15 @@ public PlatformCatalog resolvePlatformCatalog(String quarkusVersion) throws Regi return result; } + private void collectPlatforms(PlatformCatalog catalog, List collectedPlatforms, + Set collectedPlatformKeys) { + for (Platform p : catalog.getPlatforms()) { + if (collectedPlatformKeys.add(p.getPlatformKey())) { + collectedPlatforms.add(p); + } + } + } + public PlatformCatalog resolvePlatformCatalogFromRegistry(String registryId) throws RegistryResolutionException { return registries.get(getRegistryIndex(registryId)).resolvePlatformCatalog(); } @@ -301,15 +310,6 @@ public PlatformCatalog resolvePlatformCatalogFromRegistry(String registryId, Str : registries.get(getRegistryIndex(registryId)).resolvePlatformCatalog(quarkusVersion); } - private void collectPlatforms(PlatformCatalog catalog, List collectedPlatforms, - Set collectedPlatformKeys) { - for (Platform p : catalog.getPlatforms()) { - if (collectedPlatformKeys.add(p.getPlatformKey())) { - collectedPlatforms.add(p); - } - } - } - private class ExtensionCatalogBuilder { private final List catalogs = new ArrayList<>(); final Map> registriesByQuarkusCore = new HashMap<>(); @@ -320,6 +320,12 @@ void addCatalog(ExtensionCatalog c) { catalogs.add(c); } + void addUpstreamQuarkusVersion(String quarkusVersion) { + if (!upstreamQuarkusVersions.contains(quarkusVersion)) { + upstreamQuarkusVersions.add(quarkusVersion); + } + } + List getRegistriesForQuarkusCore(String quarkusVersion) { return registriesByQuarkusCore.computeIfAbsent(quarkusVersion, v -> getRegistriesForQuarkusVersion(v)); } @@ -364,41 +370,71 @@ public ExtensionCatalog resolveExtensionCatalog() throws RegistryResolutionExcep final ExtensionCatalogBuilder catalogBuilder = new ExtensionCatalogBuilder(); for (int registryIndex = 0; registryIndex < registries.size(); ++registryIndex) { - RegistryExtensionResolver registry = registries.get(registryIndex); - final PlatformCatalog pc = registry.resolvePlatformCatalog(); + collectPlatformExtensions(catalogBuilder, registryIndex); + } + + return catalogBuilder.build(); + } + + private void collectPlatformExtensions(final ExtensionCatalogBuilder catalogBuilder, int registryIndex) + throws RegistryResolutionException { + final RegistryExtensionResolver registry = registries.get(registryIndex); + + final List downstreamPreferences = new ArrayList<>(catalogBuilder.upstreamQuarkusVersions.size()); + for (String quarkusVersion : catalogBuilder.upstreamQuarkusVersions) { + if (!registry.isAcceptsQuarkusVersionQueries(quarkusVersion)) { + continue; + } + final PlatformCatalog pc = registry.resolvePlatformCatalog(quarkusVersion); if (pc == null) { continue; } - int platformIndex = 0; - for (Platform platform : pc.getPlatforms()) { - platformIndex++; - for (PlatformStream stream : platform.getStreams()) { - int releaseIndex = 0; - for (PlatformRelease release : stream.getReleases()) { - releaseIndex++; - final int compatiblityCode = catalogBuilder - .getCompatibilityCode(release.getQuarkusCoreVersion(), - release.getUpstreamQuarkusCoreVersion()); - - int memberIndex = 0; - for (ArtifactCoords bom : release.getMemberBoms()) { - memberIndex++; - final ExtensionCatalog ec = registry.resolvePlatformExtensions(bom); - if (ec != null) { - final OriginPreference originPreference = new OriginPreference(registryIndex, platformIndex, - releaseIndex, memberIndex, compatiblityCode); - addOriginPreference(ec, originPreference); - catalogBuilder.addCatalog(ec); - } else { - log.warn("Failed to resolve extension catalog for %s from registry %s", bom, registry.getId()); - } + downstreamPreferences.add(pc); + } + + PlatformCatalog pc = registry.resolvePlatformCatalog(); + if (pc == null && downstreamPreferences.isEmpty()) { + return; + } + if (!downstreamPreferences.isEmpty()) { + downstreamPreferences.add(pc); + pc = JsonCatalogMerger.mergePlatformCatalogs(downstreamPreferences); + } + + int platformIndex = 0; + for (Platform platform : pc.getPlatforms()) { + platformIndex++; + for (PlatformStream stream : platform.getStreams()) { + int releaseIndex = 0; + for (PlatformRelease release : stream.getReleases()) { + releaseIndex++; + final String quarkusVersion = release.getQuarkusCoreVersion(); + final int compatiblityCode = catalogBuilder.getCompatibilityCode(quarkusVersion, + release.getUpstreamQuarkusCoreVersion()); + + if (!registry.isExclusiveProviderOf(quarkusVersion)) { + catalogBuilder.addUpstreamQuarkusVersion(quarkusVersion); + } + if (release.getUpstreamQuarkusCoreVersion() != null) { + catalogBuilder.addUpstreamQuarkusVersion(release.getUpstreamQuarkusCoreVersion()); + } + + int memberIndex = 0; + for (ArtifactCoords bom : release.getMemberBoms()) { + memberIndex++; + final ExtensionCatalog ec = registry.resolvePlatformExtensions(bom); + if (ec != null) { + final OriginPreference originPreference = new OriginPreference(registryIndex, platformIndex, + releaseIndex, memberIndex, compatiblityCode); + addOriginPreference(ec, originPreference); + catalogBuilder.addCatalog(ec); + } else { + log.warn("Failed to resolve extension catalog for %s from registry %s", bom, registry.getId()); } } } } } - - return catalogBuilder.build(); } private void addOriginPreference(final ExtensionCatalog ec, OriginPreference originPreference) { @@ -426,25 +462,22 @@ public ExtensionCatalog resolveExtensionCatalog(String quarkusCoreVersion) throw private ExtensionCatalog resolveExtensionCatalog(String quarkusCoreVersion, final ExtensionCatalogBuilder catalogBuilder, Set preferredPlatforms) throws RegistryResolutionException { - collectPlatforms(quarkusCoreVersion, catalogBuilder, preferredPlatforms); + collectPlatformExtensions(quarkusCoreVersion, catalogBuilder, preferredPlatforms); int i = 0; while (i < catalogBuilder.upstreamQuarkusVersions.size()) { - collectPlatforms(catalogBuilder.upstreamQuarkusVersions.get(i++), catalogBuilder, preferredPlatforms); + collectPlatformExtensions(catalogBuilder.upstreamQuarkusVersions.get(i++), catalogBuilder, preferredPlatforms); } return catalogBuilder.build(); } - public ExtensionCatalog resolveExtensionCatalog(StreamCoords streamCoords) throws RegistryResolutionException { + public ExtensionCatalog resolveExtensionCatalog(PlatformStreamCoords streamCoords) throws RegistryResolutionException { ensureRegistriesConfigured(); - final ExtensionCatalogBuilder catalogBuilder = new ExtensionCatalogBuilder(); - final String platformKey = streamCoords.getPlatformKey(); final String streamId = streamCoords.getStreamId(); PlatformStream stream = null; - RegistryExtensionResolver registry = null; int registryIndex = 0; while (registryIndex < registries.size()) { final RegistryExtensionResolver qer = registries.get(registryIndex++); @@ -456,7 +489,6 @@ public ExtensionCatalog resolveExtensionCatalog(StreamCoords streamCoords) throw for (Platform p : platforms.getPlatforms()) { stream = p.getStream(streamId); if (stream != null) { - registry = qer; break; } } @@ -466,7 +498,6 @@ public ExtensionCatalog resolveExtensionCatalog(StreamCoords streamCoords) throw continue; } stream = platform.getStream(streamId); - registry = qer; } break; } @@ -510,24 +541,12 @@ public ExtensionCatalog resolveExtensionCatalog(StreamCoords streamCoords) throw throw new RegistryResolutionException(buf.toString()); } - int releaseIndex = 0; + final List catalogs = new ArrayList<>(); for (PlatformRelease release : stream.getReleases()) { - final int compatiblityCode = catalogBuilder - .getCompatibilityCode(release.getQuarkusCoreVersion(), - release.getUpstreamQuarkusCoreVersion()); - ++releaseIndex; - int memberIndex = 0; - for (ArtifactCoords bom : release.getMemberBoms()) { - final ExtensionCatalog ec = registry.resolvePlatformExtensions(bom); - final OriginPreference originPreference = new OriginPreference(registryIndex, 1, - releaseIndex, ++memberIndex, compatiblityCode); - addOriginPreference(ec, originPreference); - - catalogBuilder.addCatalog(ec); - } + catalogs.add(resolveExtensionCatalog(release.getMemberBoms())); } - return catalogBuilder.build(); + return JsonCatalogMerger.merge(catalogs); } @SuppressWarnings("unchecked") @@ -623,7 +642,7 @@ public ExtensionCatalog resolveExtensionCatalog(Collection prefe release.setMemberBoms(coords); } - collectPlatforms(quarkusVersion, catalogBuilder, registry, platformIndex, + collectPlatformExtensions(quarkusVersion, catalogBuilder, registry, platformIndex, p); continue; } @@ -688,8 +707,8 @@ private int getRegistryIndex(String registryId) { throw new IllegalStateException(buf.toString()); } - private void collectPlatforms(String quarkusCoreVersion, ExtensionCatalogBuilder catalogBuilder, - Set preferredPlatforms) + private void collectPlatformExtensions(String quarkusCoreVersion, ExtensionCatalogBuilder catalogBuilder, + Set processedPlatforms) throws RegistryResolutionException { final List quarkusVersionRegistries = catalogBuilder .getRegistriesForQuarkusCore(quarkusCoreVersion); @@ -703,18 +722,18 @@ private void collectPlatforms(String quarkusCoreVersion, ExtensionCatalogBuilder if (platforms.isEmpty()) { continue; } - int platformIndex = preferredPlatforms.size(); + int platformIndex = processedPlatforms.size(); for (Platform p : platforms) { - if (preferredPlatforms.contains(p.getPlatformKey())) { + if (processedPlatforms.contains(p.getPlatformKey())) { continue; } ++platformIndex; - collectPlatforms(quarkusCoreVersion, catalogBuilder, registry, platformIndex, p); + collectPlatformExtensions(quarkusCoreVersion, catalogBuilder, registry, platformIndex, p); } } } - private void collectPlatforms(String quarkusCoreVersion, ExtensionCatalogBuilder catalogBuilder, + private void collectPlatformExtensions(String quarkusCoreVersion, ExtensionCatalogBuilder catalogBuilder, RegistryExtensionResolver registry, int platformIndex, Platform p) throws RegistryResolutionException { for (PlatformStream s : p.getStreams()) { @@ -738,9 +757,8 @@ private void collectPlatforms(String quarkusCoreVersion, ExtensionCatalogBuilder } final String upstreamQuarkusVersion = r.getUpstreamQuarkusCoreVersion(); - if (upstreamQuarkusVersion != null - && !catalogBuilder.upstreamQuarkusVersions.contains(upstreamQuarkusVersion)) { - catalogBuilder.upstreamQuarkusVersions.add(upstreamQuarkusVersion); + if (upstreamQuarkusVersion != null) { + catalogBuilder.addUpstreamQuarkusVersion(upstreamQuarkusVersion); } } } diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/RegistryExtensionResolver.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/RegistryExtensionResolver.java index 72eda74c8fcd9..0f203f1a5dc78 100644 --- a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/RegistryExtensionResolver.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/RegistryExtensionResolver.java @@ -56,6 +56,14 @@ int checkQuarkusVersion(String quarkusVersion) { : VERSION_RECOGNIZED; } + boolean isExclusiveProviderOf(String quarkusVersion) { + return checkQuarkusVersion(quarkusVersion) == VERSION_EXCLUSIVE_PROVIDER; + } + + boolean isAcceptsQuarkusVersionQueries(String quarkusVersion) { + return checkQuarkusVersion(quarkusVersion) >= 0; + } + int checkPlatform(ArtifactCoords platform) { // TODO this should be allowed to check the full coordinates return checkQuarkusVersion(platform.getVersion()); diff --git a/independent-projects/tools/artifact-api/src/main/java/io/quarkus/maven/StreamCoords.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/PlatformStreamCoords.java similarity index 71% rename from independent-projects/tools/artifact-api/src/main/java/io/quarkus/maven/StreamCoords.java rename to independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/PlatformStreamCoords.java index 147ce22550601..8c8db274dbcca 100644 --- a/independent-projects/tools/artifact-api/src/main/java/io/quarkus/maven/StreamCoords.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/PlatformStreamCoords.java @@ -1,17 +1,17 @@ -package io.quarkus.maven; +package io.quarkus.registry.catalog; -public class StreamCoords { +public class PlatformStreamCoords { final String platformKey; final String streamId; - public static StreamCoords fromString(String stream) { + public static PlatformStreamCoords fromString(String stream) { final int colon = stream.indexOf(':'); String platformKey = colon <= 0 ? null : stream.substring(0, colon); String streamId = colon < 0 ? stream : stream.substring(colon + 1); - return new StreamCoords(platformKey, streamId); + return new PlatformStreamCoords(platformKey, streamId); } - public StreamCoords(String platformKey, String streamId) { + public PlatformStreamCoords(String platformKey, String streamId) { this.platformKey = platformKey; this.streamId = streamId; } diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/json/JsonCatalogMerger.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/json/JsonCatalogMerger.java index 4f7b79ed2695b..621565acb161b 100644 --- a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/json/JsonCatalogMerger.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/catalog/json/JsonCatalogMerger.java @@ -5,6 +5,10 @@ import io.quarkus.registry.catalog.Extension; import io.quarkus.registry.catalog.ExtensionCatalog; import io.quarkus.registry.catalog.ExtensionOrigin; +import io.quarkus.registry.catalog.Platform; +import io.quarkus.registry.catalog.PlatformCatalog; +import io.quarkus.registry.catalog.PlatformRelease; +import io.quarkus.registry.catalog.PlatformStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -72,6 +76,48 @@ public static ExtensionCatalog merge(List catalogs) { return combined; } + public static PlatformCatalog mergePlatformCatalogs(List catalogs) { + if (catalogs.isEmpty()) { + throw new IllegalArgumentException("No catalogs provided"); + } + if (catalogs.size() == 1) { + return catalogs.get(0); + } + final JsonPlatformCatalog merged = new JsonPlatformCatalog(); + final Map platformMap = new HashMap<>(); + for (PlatformCatalog c : catalogs) { + for (Platform p : c.getPlatforms()) { + final JsonPlatform mergedPlatform = platformMap.computeIfAbsent(p.getPlatformKey(), k -> { + final JsonPlatform pl = new JsonPlatform(); + pl.setPlatformKey(p.getPlatformKey()); + merged.addPlatform(pl); + return pl; + }); + for (PlatformStream s : p.getStreams()) { + JsonPlatformStream mergedStream = (JsonPlatformStream) mergedPlatform.getStream(s.getId()); + if (mergedStream == null) { + mergedStream = new JsonPlatformStream(); + mergedStream.setId(s.getId()); + mergedPlatform.addStream(mergedStream); + } + for (PlatformRelease r : s.getReleases()) { + final PlatformRelease release = mergedStream.getRelease(r.getVersion()); + if (release == null) { + mergedStream.addRelease(r); + } + } + final Map mergedStreamMetadata = mergedStream.getMetadata(); + s.getMetadata().entrySet() + .forEach(entry -> mergedStreamMetadata.putIfAbsent(entry.getKey(), entry.getValue())); + } + p.getMetadata().entrySet() + .forEach(entry -> mergedPlatform.getMetadata().putIfAbsent(entry.getKey(), entry.getValue())); + } + c.getMetadata().entrySet().forEach(entry -> merged.getMetadata().putIfAbsent(entry.getKey(), entry.getValue())); + } + return merged; + } + private static List detectRoots(List catalogs) { final Set allDerivedFrom = new HashSet<>(catalogs.size()); for (ExtensionCatalog catalog : catalogs) { diff --git a/independent-projects/tools/registry-client/src/test/java/io/quarkus/registry/catalog/json/JsonCatalogMergerTest.java b/independent-projects/tools/registry-client/src/test/java/io/quarkus/registry/catalog/json/JsonCatalogMergerTest.java new file mode 100644 index 0000000000000..0026ad35ba936 --- /dev/null +++ b/independent-projects/tools/registry-client/src/test/java/io/quarkus/registry/catalog/json/JsonCatalogMergerTest.java @@ -0,0 +1,99 @@ +package io.quarkus.registry.catalog.json; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.quarkus.maven.ArtifactCoords; +import io.quarkus.registry.catalog.Platform; +import io.quarkus.registry.catalog.PlatformCatalog; +import io.quarkus.registry.catalog.PlatformRelease; +import io.quarkus.registry.catalog.PlatformStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class JsonCatalogMergerTest { + + @Test + void testMergePlatformCatalogs() throws Exception { + + final List catalogs = new ArrayList<>(); + + JsonPlatformCatalog c = new JsonPlatformCatalog(); + catalogs.add(c); + JsonPlatform p = new JsonPlatform(); + c.addPlatform(p); + p.setPlatformKey("platform"); + JsonPlatformStream s = new JsonPlatformStream(); + s.setId("2.0"); + p.addStream(s); + JsonPlatformRelease r = new JsonPlatformRelease(); + r.setQuarkusCoreVersion("2.0.0"); + r.setVersion(JsonPlatformReleaseVersion.fromString("2.2.2")); + r.setMemberBoms(Collections.singletonList(ArtifactCoords.fromString("org.acme:acme-quarkus-bom::pom:2.2.2"))); + s.addRelease(r); + + s = new JsonPlatformStream(); + s.setId("1.0"); + p.addStream(s); + r = new JsonPlatformRelease(); + r.setQuarkusCoreVersion("1.0.1"); + r.setVersion(JsonPlatformReleaseVersion.fromString("1.1.2")); + r.setMemberBoms(Collections.singletonList(ArtifactCoords.fromString("org.acme:acme-quarkus-bom::pom:1.1.2"))); + s.addRelease(r); + + c = new JsonPlatformCatalog(); + catalogs.add(c); + p = new JsonPlatform(); + c.addPlatform(p); + p.setPlatformKey("platform"); + s = new JsonPlatformStream(); + s.setId("2.0"); + p.addStream(s); + r = new JsonPlatformRelease(); + r.setQuarkusCoreVersion("2.0.1"); + r.setVersion(JsonPlatformReleaseVersion.fromString("2.2.3")); + r.setMemberBoms(Collections.singletonList(ArtifactCoords.fromString("org.acme:acme-quarkus-bom::pom:2.2.3"))); + s.addRelease(r); + + s = new JsonPlatformStream(); + s.setId("1.0"); + p.addStream(s); + r = new JsonPlatformRelease(); + r.setQuarkusCoreVersion("1.0.0"); + r.setVersion(JsonPlatformReleaseVersion.fromString("1.1.1")); + r.setMemberBoms(Collections.singletonList(ArtifactCoords.fromString("org.acme:acme-quarkus-bom::pom:1.1.1"))); + s.addRelease(r); + + final PlatformCatalog merged = JsonCatalogMerger.mergePlatformCatalogs(catalogs); + Collection platforms = merged.getPlatforms(); + assertThat(platforms.size()).isEqualTo(1); + Platform platform = platforms.iterator().next(); + assertThat(platform.getPlatformKey()).isEqualTo("platform"); + assertThat(platform.getStreams().size()).isEqualTo(2); + Iterator streams = platform.getStreams().iterator(); + PlatformStream stream = streams.next(); + assertThat(stream.getId()).isEqualTo("2.0"); + assertThat(stream.getReleases().size()).isEqualTo(2); + Iterator releases = stream.getReleases().iterator(); + PlatformRelease release = releases.next(); + assertThat(release.getVersion()).isEqualTo(JsonPlatformReleaseVersion.fromString("2.2.2")); + assertThat(release.getQuarkusCoreVersion()).isEqualTo("2.0.0"); + release = releases.next(); + assertThat(release.getVersion()).isEqualTo(JsonPlatformReleaseVersion.fromString("2.2.3")); + assertThat(release.getQuarkusCoreVersion()).isEqualTo("2.0.1"); + + stream = streams.next(); + assertThat(stream.getId()).isEqualTo("1.0"); + assertThat(stream.getReleases().size()).isEqualTo(2); + releases = stream.getReleases().iterator(); + release = releases.next(); + assertThat(release.getVersion()).isEqualTo(JsonPlatformReleaseVersion.fromString("1.1.2")); + assertThat(release.getQuarkusCoreVersion()).isEqualTo("1.0.1"); + release = releases.next(); + assertThat(release.getVersion()).isEqualTo(JsonPlatformReleaseVersion.fromString("1.1.1")); + assertThat(release.getQuarkusCoreVersion()).isEqualTo("1.0.0"); + } +} diff --git a/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/build.gradle b/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/build.gradle new file mode 100644 index 0000000000000..ed525b0701145 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' + id 'io.quarkus' +} + +repositories { + if (System.properties.containsKey('maven.repo.local')) { + maven { + url System.properties.get('maven.repo.local') + } + } else { + mavenLocal() + } + mavenCentral() +} + +dependencies { + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy' + implementation 'io.quarkus:quarkus-kotlin' +} + +compileJava { + options.compilerArgs << '-parameters' +} + +// This is a fix as kotlin 1.5.30 does not support Java 17 yet +tasks.quarkusDev { + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { + compilerArgs = ["-jvm-target", "16"] + } else { + compilerArgs = ["-jvm-target", "11"] + } +} diff --git a/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/gradle.properties b/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/gradle.properties new file mode 100644 index 0000000000000..15d78eaf882e5 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/gradle.properties @@ -0,0 +1,4 @@ +kotlinVersion=1.5.30 + +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/settings.gradle b/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/settings.gradle new file mode 100644 index 0000000000000..fb99dc799e1e2 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + if (System.properties.containsKey('maven.repo.local')) { + maven { + url System.properties.get('maven.repo.local') + } + } else { + mavenLocal() + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + id 'org.jetbrains.kotlin.jvm' version "${kotlinVersion}" + } +} +rootProject.name='code-with-quarkus' diff --git a/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/src/main/kotlin/org/acme/GreetingResource.kt b/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/src/main/kotlin/org/acme/GreetingResource.kt new file mode 100644 index 0000000000000..7634ba1421904 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-kotlin-application-project/src/main/kotlin/org/acme/GreetingResource.kt @@ -0,0 +1,16 @@ +package org.acme + +import javax.ws.rs.GET +import javax.ws.rs.Path +import javax.ws.rs.Produces +import javax.ws.rs.core.MediaType + +@Path("/hello") +class GreetingResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + fun hello(): String { + return "hello" + } +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies/ext-a/deployment/build.gradle b/integration-tests/gradle/src/main/resources/conditional-dependencies/ext-a/deployment/build.gradle index e1df700721ca6..9ad3b5e14a594 100644 --- a/integration-tests/gradle/src/main/resources/conditional-dependencies/ext-a/deployment/build.gradle +++ b/integration-tests/gradle/src/main/resources/conditional-dependencies/ext-a/deployment/build.gradle @@ -6,7 +6,7 @@ plugins { dependencies { implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") implementation 'io.quarkus:quarkus-core-deployment' - implementation("org.hibernate:hibernate-core:5.4.9") + implementation("org.acme:simple-dependency:1.0-SNAPSHOT") implementation project(':ext-a:runtime') } diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies/ext-a/runtime/build.gradle b/integration-tests/gradle/src/main/resources/conditional-dependencies/ext-a/runtime/build.gradle index 74cd2b2fa4622..76e136b702273 100644 --- a/integration-tests/gradle/src/main/resources/conditional-dependencies/ext-a/runtime/build.gradle +++ b/integration-tests/gradle/src/main/resources/conditional-dependencies/ext-a/runtime/build.gradle @@ -4,10 +4,11 @@ plugins { } dependencies { - implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") - api("org.hibernate:hibernate-core:5.4.9") { - exclude module: "byte-buddy" + implementation platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + api("org.acme:simple-dependency:1.0-SNAPSHOT") { + exclude module: "transitive-dependency" } + api("io.quarkus:quarkus-hibernate-reactive-panache") } publishing { diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies/settings.gradle b/integration-tests/gradle/src/main/resources/conditional-dependencies/settings.gradle index 787375fe580ea..f51da33f65e6c 100644 --- a/integration-tests/gradle/src/main/resources/conditional-dependencies/settings.gradle +++ b/integration-tests/gradle/src/main/resources/conditional-dependencies/settings.gradle @@ -35,4 +35,5 @@ include 'ext-p:runtime', 'ext-p:deployment' include 'ext-r:runtime', 'ext-r:deployment' include 'ext-s:runtime', 'ext-s:deployment' include 'ext-t:runtime', 'ext-t:deployment' -include 'ext-u:runtime', 'ext-u:deployment' \ No newline at end of file +include 'ext-u:runtime', 'ext-u:deployment' +include 'simple-dependency', 'transitive-dependency' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies/simple-dependency/build.gradle b/integration-tests/gradle/src/main/resources/conditional-dependencies/simple-dependency/build.gradle new file mode 100644 index 0000000000000..05847f8c1dcb0 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/conditional-dependencies/simple-dependency/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java-library' + id 'maven-publish' +} + +repositories { + mavenCentral() + // in case a custom local repo is configured we are going to use that instead of the default mavenLocal() + if (System.properties.containsKey('maven.repo.local')) { + maven { + url System.properties.get('maven.repo.local') + } + } else { + mavenLocal() + } +} + +dependencies { + api project(":transitive-dependency") +} + +publishing { + publications { + mavenJava(MavenPublication) { + groupId = 'org.acme' + artifactId = 'simple-dependency' + version = '1.0-SNAPSHOT' + + from components.java + } + } +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies/simple-dependency/settings.gradle b/integration-tests/gradle/src/main/resources/conditional-dependencies/simple-dependency/settings.gradle new file mode 100644 index 0000000000000..bc22fa18a0e22 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/conditional-dependencies/simple-dependency/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + // in case a custom local repo is configured we are going to use that instead of the default mavenLocal() + if (System.properties.containsKey('maven.repo.local')) { + maven { + url System.properties.get('maven.repo.local') + } + } else { + mavenLocal() + } + } +} + +rootProject.name = 'simple-dependency' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies/simple-dependency/src/main/resources/META-INF/beans.xml b/integration-tests/gradle/src/main/resources/conditional-dependencies/simple-dependency/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies/transitive-dependency/build.gradle b/integration-tests/gradle/src/main/resources/conditional-dependencies/transitive-dependency/build.gradle new file mode 100644 index 0000000000000..3a9ec43ebee1e --- /dev/null +++ b/integration-tests/gradle/src/main/resources/conditional-dependencies/transitive-dependency/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java' + id 'maven-publish' +} + +repositories { + mavenCentral() + // in case a custom local repo is configured we are going to use that instead of the default mavenLocal() + if (System.properties.containsKey('maven.repo.local')) { + maven { + url System.properties.get('maven.repo.local') + } + } else { + mavenLocal() + } +} + +publishing { + publications { + mavenJava(MavenPublication) { + groupId = 'org.acme' + artifactId = 'transitive-dependency' + version = '1.0-SNAPSHOT' + + from components.java + } + } +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies/transitive-dependency/settings.gradle b/integration-tests/gradle/src/main/resources/conditional-dependencies/transitive-dependency/settings.gradle new file mode 100644 index 0000000000000..b83ad94f36a32 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/conditional-dependencies/transitive-dependency/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + // in case a custom local repo is configured we are going to use that instead of the default mavenLocal() + if (System.properties.containsKey('maven.repo.local')) { + maven { + url System.properties.get('maven.repo.local') + } + } else { + mavenLocal() + } + } +} + +rootProject.name = 'transitive-dependency' \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/conditional-dependencies/transitive-dependency/src/main/resources/META-INF/beans.xml b/integration-tests/gradle/src/main/resources/conditional-dependencies/transitive-dependency/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/gradle/src/main/resources/multi-module-kotlin-project/web/build.gradle.kts b/integration-tests/gradle/src/main/resources/multi-module-kotlin-project/web/build.gradle.kts index dd4627b9f7240..55afb0a79cdbb 100644 --- a/integration-tests/gradle/src/main/resources/multi-module-kotlin-project/web/build.gradle.kts +++ b/integration-tests/gradle/src/main/resources/multi-module-kotlin-project/web/build.gradle.kts @@ -3,6 +3,12 @@ plugins { kotlin("jvm") } +// This is a fix as kotlin 1.5.30 does not support Java 17 yet +if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { + tasks.quarkusDev { + compilerArgs = listOf("-jvm-target", "16") + } +} dependencies { implementation(project(":port")) implementation(project(":domain")) diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/ConditionalDependenciesTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/ConditionalDependenciesTest.java index b968f724f5b5c..4dacdd806b78e 100644 --- a/integration-tests/gradle/src/test/java/io/quarkus/gradle/ConditionalDependenciesTest.java +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/ConditionalDependenciesTest.java @@ -22,7 +22,10 @@ public class ConditionalDependenciesTest extends QuarkusGradleWrapperTestBase { @Order(1) public void publishTestExtensions() throws IOException, InterruptedException, URISyntaxException { File dependencyProject = getProjectDir("conditional-dependencies"); - runGradleWrapper(dependencyProject, ":ext-a:runtime:publishToMavenLocal", + runGradleWrapper(dependencyProject, ":transitive-dependency:publishToMavenLocal", + ":simple-dependency:publishToMavenLocal"); + runGradleWrapper(dependencyProject, + ":ext-a:runtime:publishToMavenLocal", ":ext-a:deployment:publishToMavenLocal", ":ext-b:runtime:publishToMavenLocal", ":ext-b:deployment:publishToMavenLocal", @@ -79,10 +82,11 @@ public void shouldImportConditionalDependency() throws IOException, URISyntaxExc assertThat(mainLib.resolve("org.acme.ext-c-1.0-SNAPSHOT.jar")).exists(); assertThat(mainLib.resolve("org.acme.ext-e-1.0-SNAPSHOT.jar")).exists(); assertThat(mainLib.resolve("org.acme.ext-d-1.0-SNAPSHOT.jar")).doesNotExist(); - assertThat(mainLib.resolve("net.bytebuddy.byte-buddy-1.11.12.jar")).doesNotExist(); + assertThat(mainLib.resolve("org.acme.transitive-dependency-1.0-SNAPSHOT.jar")).doesNotExist(); final Path deploymentLib = buildDir.toPath().resolve("quarkus-app").resolve("lib").resolve("deployment"); - assertThat(deploymentLib.resolve("net.bytebuddy.byte-buddy-1.11.12.jar")).exists(); + assertThat(deploymentLib.resolve("org.acme.transitive-dependency-1.0-SNAPSHOT.jar")).exists(); + assertThat(deploymentLib.resolve("io.quarkus.quarkus-agroal-" + getQuarkusVersion() + ".jar")).doesNotExist(); } @Test diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/devmode/BasicKotlinApplicationModuleDevModeTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/devmode/BasicKotlinApplicationModuleDevModeTest.java new file mode 100644 index 0000000000000..a98c32842b24b --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/devmode/BasicKotlinApplicationModuleDevModeTest.java @@ -0,0 +1,26 @@ +package io.quarkus.gradle.devmode; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.UUID; + +import com.google.common.collect.ImmutableMap; + +public class BasicKotlinApplicationModuleDevModeTest extends QuarkusDevGradleTestBase { + + @Override + protected String projectDirectoryName() { + return "basic-kotlin-application-project"; + } + + @Override + protected void testDevMode() throws Exception { + assertThat(getHttpResponse("/hello")).contains("hello"); + + final String uuid = UUID.randomUUID().toString(); + replace("src/main/kotlin/org/acme/GreetingResource.kt", + ImmutableMap.of("return \"hello\"", "return \"" + uuid + "\"")); + + assertUpdatedResponseContains("/hello", uuid); + } +} diff --git a/integration-tests/hibernate-validator-resteasy-reactive/pom.xml b/integration-tests/hibernate-validator-resteasy-reactive/pom.xml new file mode 100644 index 0000000000000..9862dac97f3d3 --- /dev/null +++ b/integration-tests/hibernate-validator-resteasy-reactive/pom.xml @@ -0,0 +1,182 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-integration-test-hibernate-validator-resteasy-reactive + Quarkus - Integration Tests - Hibernate Validator + Module that contains Hibernate Validator/Bean Validation related tests using RESTEasy Reactive + + + + io.quarkus + quarkus-resteasy-reactive-jsonb + + + io.quarkus + quarkus-hibernate-validator + + + io.quarkus + quarkus-arc + + + + + io.quarkus + quarkus-hibernate-orm + + + io.quarkus + quarkus-jdbc-h2 + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-test-h2 + test + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-hibernate-orm-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-hibernate-validator-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-jdbc-h2-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-jsonb-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + src/main/resources + true + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + en + + + + + + + + + native-image + + + native + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + en + + + + + + + + + diff --git a/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/HibernateValidatorTestResource.java b/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/HibernateValidatorTestResource.java new file mode 100644 index 0000000000000..cc7f0929c2e14 --- /dev/null +++ b/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/HibernateValidatorTestResource.java @@ -0,0 +1,167 @@ +package io.quarkus.it.hibernate.validator; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.validation.ConstraintViolation; +import javax.validation.Valid; +import javax.validation.Validator; +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.Email; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.hibernate.validator.constraints.Length; + +import io.quarkus.it.hibernate.validator.custom.MyOtherBean; +import io.quarkus.runtime.StartupEvent; + +@Path("/hibernate-validator/test") +public class HibernateValidatorTestResource { + + @Inject + Validator validator; + + public void testValidationOutsideOfResteasyContext(@Observes StartupEvent startupEvent) { + validator.validate(new MyOtherBean(null)); + } + + @GET + @Path("/basic-features") + @Produces(MediaType.TEXT_PLAIN) + public String testBasicFeatures() { + ResultBuilder result = new ResultBuilder(); + + Map> invalidCategorizedEmails = new HashMap<>(); + invalidCategorizedEmails.put("a", Collections.singletonList("b")); + + result.append(formatViolations(validator.validate(new MyBean( + "Bill Jones", + "b", + Collections.singletonList("c"), + -4d, + invalidCategorizedEmails)))); + + Map> validCategorizedEmails = new HashMap<>(); + validCategorizedEmails.put("Professional", Collections.singletonList("bill.jones@example.com")); + + result.append(formatViolations(validator.validate(new MyBean( + "Bill Jones", + "bill.jones@example.com", + Collections.singletonList("biji@example.com"), + 5d, + validCategorizedEmails)))); + + return result.build(); + } + + private String formatViolations(Set> violations) { + if (violations.isEmpty()) { + return "passed"; + } + + return "failed: " + violations.stream() + .map(v -> v.getPropertyPath().toString() + " (" + v.getMessage() + ")") + .sorted() + .collect(Collectors.joining(", ")); + } + + public static class MyBean { + + private String name; + + @Email + private String email; + + private List<@Email String> additionalEmails; + + @DecimalMin("0") + private Double score; + + private Map<@Length(min = 3) String, List<@Email String>> categorizedEmails; + + @Valid + private NestedBeanWithoutConstraints nestedBeanWithoutConstraints; + + public MyBean(String name, String email, List additionalEmails, Double score, + Map> categorizedEmails) { + this.name = name; + this.email = email; + this.additionalEmails = additionalEmails; + this.score = score; + this.categorizedEmails = categorizedEmails; + this.nestedBeanWithoutConstraints = new NestedBeanWithoutConstraints(); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public List getAdditionalEmails() { + return additionalEmails; + } + + public void setAdditionalEmails(List additionalEmails) { + this.additionalEmails = additionalEmails; + } + + public Double getScore() { + return score; + } + + public void setScore(Double score) { + this.score = score; + } + + public Map> getCategorizedEmails() { + return categorizedEmails; + } + + public void setCategorizedEmails(Map> categorizedEmails) { + this.categorizedEmails = categorizedEmails; + } + } + + private static class ResultBuilder { + + private StringBuilder builder = new StringBuilder(); + + public ResultBuilder append(String element) { + if (builder.length() > 0) { + builder.append("\n"); + } + builder.append(element); + return this; + } + + public String build() { + return builder.toString(); + } + } + + private static class NestedBeanWithoutConstraints { + + @SuppressWarnings("unused") + private String property; + } +} diff --git a/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/HibernateValidatorTestResourceGenericInterface.java b/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/HibernateValidatorTestResourceGenericInterface.java new file mode 100644 index 0000000000000..d2dfd4aa2222f --- /dev/null +++ b/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/HibernateValidatorTestResourceGenericInterface.java @@ -0,0 +1,9 @@ +package io.quarkus.it.hibernate.validator; + +import javax.validation.constraints.Digits; + +public interface HibernateValidatorTestResourceGenericInterface { + + T testRestEndpointGenericMethodValidation(@Digits(integer = 5, fraction = 0) T id); + +} diff --git a/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/HibernateValidatorTestResourceInterface.java b/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/HibernateValidatorTestResourceInterface.java new file mode 100644 index 0000000000000..fe76470c7c252 --- /dev/null +++ b/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/HibernateValidatorTestResourceInterface.java @@ -0,0 +1,22 @@ +package io.quarkus.it.hibernate.validator; + +import javax.validation.constraints.Digits; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +public interface HibernateValidatorTestResourceInterface { + + @GET + @Path("/rest-end-point-interface-validation/{id}/") + @Produces(MediaType.TEXT_PLAIN) + public String testRestEndPointInterfaceValidation(@Digits(integer = 5, fraction = 0) @PathParam("id") String id); + + @GET + @Path("/rest-end-point-interface-validation-annotation-on-impl-method/{id}/") + @Produces(MediaType.TEXT_PLAIN) + public String testRestEndPointInterfaceValidationWithAnnotationOnImplMethod( + @Digits(integer = 5, fraction = 0) @PathParam("id") String id); +} diff --git a/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/custom/MyCustomConstraint.java b/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/custom/MyCustomConstraint.java new file mode 100644 index 0000000000000..ad412dea04eb5 --- /dev/null +++ b/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/custom/MyCustomConstraint.java @@ -0,0 +1,31 @@ +package io.quarkus.it.hibernate.validator.custom; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.Payload; + +@Target({ ElementType.TYPE_USE, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = MyCustomConstraint.Validator.class) +public @interface MyCustomConstraint { + + String message() default "{MyCustomConstraint.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + class Validator implements ConstraintValidator { + + @Override + public boolean isValid(MyOtherBean value, ConstraintValidatorContext context) { + return value.getName() != null; + } + } +} diff --git a/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/custom/MyOtherBean.java b/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/custom/MyOtherBean.java new file mode 100644 index 0000000000000..4961a6d0103b0 --- /dev/null +++ b/integration-tests/hibernate-validator-resteasy-reactive/src/main/java/io/quarkus/it/hibernate/validator/custom/MyOtherBean.java @@ -0,0 +1,19 @@ +package io.quarkus.it.hibernate.validator.custom; + +@MyCustomConstraint +public class MyOtherBean { + + private String name; + + public MyOtherBean(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java b/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java new file mode 100644 index 0000000000000..433213be9fad8 --- /dev/null +++ b/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java @@ -0,0 +1,31 @@ +package io.quarkus.it.hibernate.validator; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.h2.H2DatabaseTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +@QuarkusTestResource(H2DatabaseTestResource.class) +public class HibernateValidatorFunctionalityTest { + + @Test + public void testBasicFeatures() throws Exception { + StringBuilder expected = new StringBuilder(); + expected.append("failed: additionalEmails[0]. (must be a well-formed email address)").append(", ") + .append("categorizedEmails[a]. (length must be between 3 and 2147483647)").append(", ") + .append("categorizedEmails[a].[0]. (must be a well-formed email address)").append(", ") + .append("email (must be a well-formed email address)").append(", ") + .append("score (must be greater than or equal to 0)").append("\n"); + expected.append("passed"); + + RestAssured.when() + .get("/hibernate-validator/test/basic-features") + .then() + .body(is(expected.toString())); + } +} diff --git a/integration-tests/jpa-oracle/src/main/resources/application.properties b/integration-tests/jpa-oracle/src/main/resources/application.properties index 7c08376aa4a75..0abd503fde3ec 100644 --- a/integration-tests/jpa-oracle/src/main/resources/application.properties +++ b/integration-tests/jpa-oracle/src/main/resources/application.properties @@ -4,4 +4,4 @@ quarkus.datasource.password=hibernate_orm_test quarkus.datasource.jdbc.url=${oracledb.url} quarkus.datasource.jdbc.max-size=1 quarkus.hibernate-orm.database.generation=drop-and-create -quarkus.datasource.jdbc.additional-jdbc-properties."oracle.jdbc.timezoneAsRegion"=false \ No newline at end of file +quarkus.datasource.jdbc.additional-jdbc-properties."oracle.jdbc.timezoneAsRegion"=false diff --git a/integration-tests/main/src/main/java/io/quarkus/it/websocket/AddWebSocketHandler.java b/integration-tests/main/src/main/java/io/quarkus/it/websocket/AddWebSocketHandler.java new file mode 100644 index 0000000000000..93dd2b6222880 --- /dev/null +++ b/integration-tests/main/src/main/java/io/quarkus/it/websocket/AddWebSocketHandler.java @@ -0,0 +1,41 @@ +package io.quarkus.it.websocket; + +import javax.enterprise.inject.spi.DeploymentException; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.annotation.WebListener; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.Session; +import javax.websocket.server.ServerContainer; +import javax.websocket.server.ServerEndpointConfig; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@WebListener +public class AddWebSocketHandler implements ServletContextListener { + @Override + public void contextDestroyed(ServletContextEvent sce) { + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + try { + ((ServerContainer) sce.getServletContext().getAttribute(ServerContainer.class.getName())) + .addEndpoint(ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/added-dynamic").build()); + + } catch (DeploymentException | javax.websocket.DeploymentException e) { + throw new RuntimeException(e); + } + } + + @RegisterForReflection + public static class WebSocketEndpoint extends Endpoint { + + @Override + public void onOpen(Session session, EndpointConfig config) { + session.getAsyncRemote().sendText("DYNAMIC"); + } + + } +} diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/JaxbTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/JaxbTestCase.java index 62110d4baeb4a..8c32cf33add5c 100644 --- a/integration-tests/main/src/test/java/io/quarkus/it/main/JaxbTestCase.java +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/JaxbTestCase.java @@ -2,11 +2,6 @@ import static org.hamcrest.Matchers.contains; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; @@ -15,27 +10,9 @@ @QuarkusTest public class JaxbTestCase { - private static final AtomicInteger count = new AtomicInteger(0); - - @BeforeEach - public void beforeInEnclosing() { - count.incrementAndGet(); + @Test + public void testNews() { + RestAssured.when().get("/test/jaxb/getnews").then() + .body("author", contains("Emmanuel Bernard")); } - - @Nested - class SomeClass { - - @BeforeEach - public void beforeInTest() { - count.incrementAndGet(); - } - - @Test - public void testNews() { - RestAssured.when().get("/test/jaxb/getnews").then() - .body("author", contains("Emmanuel Bernard")); - Assertions.assertEquals(2, count.get()); - } - } - } diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/QuarkusTestCallbacksTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/QuarkusTestCallbacksTestCase.java index af121205b21c2..80983c0f53bc0 100644 --- a/integration-tests/main/src/test/java/io/quarkus/it/main/QuarkusTestCallbacksTestCase.java +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/QuarkusTestCallbacksTestCase.java @@ -3,6 +3,7 @@ import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -11,7 +12,9 @@ import java.lang.annotation.Target; import java.lang.reflect.Method; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -27,6 +30,22 @@ @QuarkusTest public class QuarkusTestCallbacksTestCase { + @BeforeAll + static void beforeAllWithTestInfo(TestInfo testInfo) { + checkBeforeOrAfterAllTestInfo(testInfo); + } + + @AfterAll + static void afterAllWithTestInfo(TestInfo testInfo) { + checkBeforeOrAfterAllTestInfo(testInfo); + } + + private static void checkBeforeOrAfterAllTestInfo(TestInfo testInfo) { + assertNotNull(testInfo); + assertEquals(QuarkusTestCallbacksTestCase.class, testInfo.getTestClass().get()); + assertFalse(testInfo.getTestMethod().isPresent()); + } + @BeforeEach void beforeEachWithTestInfo(TestInfo testInfo) throws NoSuchMethodException { checkBeforeOrAfterEachTestInfo(testInfo, "beforeEachWithTestInfo"); @@ -43,6 +62,7 @@ private void checkBeforeOrAfterEachTestInfo(TestInfo testInfo, String unexpected assertNotEquals(testMethodName, QuarkusTestCallbacksTestCase.class.getDeclaredMethod(unexpectedMethodName, TestInfo.class)); assertTrue(testMethodName.startsWith("test")); + assertEquals(QuarkusTestCallbacksTestCase.class, testInfo.getTestClass().get()); } @Test @@ -66,6 +86,7 @@ public void testInfoTestCase(TestInfo testInfo) throws NoSuchMethodException { Method testMethod = testInfo.getTestMethod().get(); assertEquals(testMethod, QuarkusTestCallbacksTestCase.class.getDeclaredMethod("testInfoTestCase", TestInfo.class)); assertEquals(1, testMethod.getAnnotationsByType(TestAnnotation.class).length); + assertEquals(QuarkusTestCallbacksTestCase.class, testInfo.getTestClass().get()); } @Target({ METHOD }) diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/QuarkusTestNestedTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/QuarkusTestNestedTestCase.java new file mode 100644 index 0000000000000..30bb5e2fc1430 --- /dev/null +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/QuarkusTestNestedTestCase.java @@ -0,0 +1,127 @@ +package io.quarkus.it.main; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import io.quarkus.test.junit.QuarkusTest; + +/** + * Tests {@link Nested} support of {@link QuarkusTest}. Notes: + *
    + *
  • to avoid unexpected execution order, don't use surefire's {@code -Dtest=...}, use {@code -Dgroups=nested} instead
  • + *
  • order of nested test classes is reversed by JUnit (and there's no way to enforce a specific order)
  • + *
+ */ +@QuarkusTest +@Tag("nested") +public class QuarkusTestNestedTestCase { + + private static final AtomicInteger COUNT_BEFORE_ALL = new AtomicInteger(0); + private static final AtomicInteger COUNT_BEFORE_EACH = new AtomicInteger(0); + private static final AtomicInteger COUNT_TEST = new AtomicInteger(0); + private static final AtomicInteger COUNT_AFTER_EACH = new AtomicInteger(0); + private static final AtomicInteger COUNT_AFTER_ALL = new AtomicInteger(0); + + @BeforeAll + static void beforeAll() { + COUNT_BEFORE_ALL.incrementAndGet(); + } + + @BeforeEach + void beforeEach() { + COUNT_BEFORE_EACH.incrementAndGet(); + } + + @Test + void test() { + assertEquals(1, COUNT_BEFORE_ALL.get(), "COUNT_BEFORE_ALL"); + assertEquals(1, COUNT_BEFORE_EACH.get(), "COUNT_BEFORE_EACH"); + assertEquals(0, COUNT_TEST.getAndIncrement(), "COUNT_TEST"); + assertEquals(0, COUNT_AFTER_EACH.get(), "COUNT_AFTER_EACH"); + assertEquals(0, COUNT_AFTER_ALL.get(), "COUNT_AFTER_ALL"); + } + + @Nested + @TestMethodOrder(OrderAnnotation.class) + class FirstNested { + + @BeforeEach + void beforeEach() { + COUNT_BEFORE_EACH.incrementAndGet(); + } + + @Test + @Order(1) + void testOne() { + assertEquals(1, COUNT_BEFORE_ALL.get(), "COUNT_BEFORE_ALL"); + assertEquals(5, COUNT_BEFORE_EACH.get(), "COUNT_BEFORE_EACH"); + assertEquals(2, COUNT_TEST.getAndIncrement(), "COUNT_TEST"); + assertEquals(3, COUNT_AFTER_EACH.get(), "COUNT_AFTER_EACH"); + assertEquals(0, COUNT_AFTER_ALL.get(), "COUNT_AFTER_ALL"); + } + + @Test + @Order(2) + void testTwo() { + assertEquals(1, COUNT_BEFORE_ALL.get(), "COUNT_BEFORE_ALL"); + assertEquals(7, COUNT_BEFORE_EACH.get(), "COUNT_BEFORE_EACH"); + assertEquals(3, COUNT_TEST.getAndIncrement(), "COUNT_TEST"); + assertEquals(5, COUNT_AFTER_EACH.get(), "COUNT_AFTER_EACH"); + assertEquals(0, COUNT_AFTER_ALL.get(), "COUNT_AFTER_ALL"); + } + + @AfterEach + void afterEach() { + COUNT_AFTER_EACH.incrementAndGet(); + } + } + + @Nested + class SecondNested { + + @BeforeEach + void beforeEach() { + COUNT_BEFORE_EACH.incrementAndGet(); + } + + @Test + void testOne() { + assertEquals(1, COUNT_BEFORE_ALL.get(), "COUNT_BEFORE_ALL"); + assertEquals(3, COUNT_BEFORE_EACH.get(), "COUNT_BEFORE_EACH"); + assertEquals(1, COUNT_TEST.getAndIncrement(), "COUNT_TEST"); + assertEquals(1, COUNT_AFTER_EACH.get(), "COUNT_AFTER_EACH"); + assertEquals(0, COUNT_AFTER_ALL.get(), "COUNT_AFTER_ALL"); + } + + @AfterEach + void afterEach() { + COUNT_AFTER_EACH.incrementAndGet(); + } + } + + @AfterEach + void afterEach() { + COUNT_AFTER_EACH.incrementAndGet(); + } + + @AfterAll + static void afterAll() { + assertEquals(1, COUNT_BEFORE_ALL.get(), "COUNT_BEFORE_ALL"); + assertEquals(7, COUNT_BEFORE_EACH.get(), "COUNT_BEFORE_EACH"); + assertEquals(4, COUNT_TEST.get(), "COUNT_TEST"); + assertEquals(7, COUNT_AFTER_EACH.get(), "COUNT_AFTER_EACH"); + assertEquals(0, COUNT_AFTER_ALL.getAndIncrement(), "COUNT_AFTER_ALL"); + } +} diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/WebsocketTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/WebsocketTestCase.java index 2fcf61f794671..e92668b8eb883 100644 --- a/integration-tests/main/src/test/java/io/quarkus/it/main/WebsocketTestCase.java +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/WebsocketTestCase.java @@ -32,6 +32,8 @@ public class WebsocketTestCase { @TestHTTPResource("wsopen") URI openURI; + @TestHTTPResource("added-dynamic") + URI added; @Test public void websocketTest() throws Exception { @@ -57,6 +59,29 @@ public void onMessage(String s) { } } + @Test + public void addedWebSocketTest() throws Exception { + + LinkedBlockingDeque message = new LinkedBlockingDeque<>(); + Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig endpointConfig) { + session.addMessageHandler(new MessageHandler.Whole() { + @Override + public void onMessage(String s) { + message.add(s); + } + }); + } + }, ClientEndpointConfig.Builder.create().build(), added); + + try { + Assertions.assertEquals("DYNAMIC", message.poll(20, TimeUnit.SECONDS)); + } finally { + session.close(); + } + } + @Test public void websocketServerEncodingAndDecodingTest() throws Exception { LinkedBlockingDeque message = new LinkedBlockingDeque<>(); diff --git a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/PathTemplateResource.java b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/PathTemplateResource.java new file mode 100644 index 0000000000000..721e384a04300 --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/PathTemplateResource.java @@ -0,0 +1,13 @@ +package io.quarkus.it.micrometer.prometheus; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +@Path("template/path/{value}") +public class PathTemplateResource { + @GET + public String get(@PathParam("value") String value) { + return "Received: " + value; + } +} diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java index 595ed7ed54bfa..2a76940f1e80d 100644 --- a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java @@ -77,6 +77,13 @@ void testAllTheThings() { .body(containsString("OK")); } + @Test + @Order(9) + void testTemplatedPathOnClass() { + when().get("/template/path/anything").then().statusCode(200) + .body(containsString("Received: anything")); + } + @Test @Order(10) void testPrometheusScrapeEndpoint() { @@ -103,6 +110,9 @@ void testPrometheusScrapeEndpoint() { .body(containsString("uri=\"/message/match/{id}/{sub}\"")) .body(containsString("uri=\"/message/match/{other}\"")) + .body(containsString( + "http_server_requests_seconds_count{env=\"test\",method=\"GET\",outcome=\"SUCCESS\",registry=\"prometheus\",status=\"200\",uri=\"/template/path/{value}\"")) + // Verify Hibernate Metrics .body(containsString( "hibernate_sessions_open_total{entityManagerFactory=\"\",env=\"test\",registry=\"prometheus\",} 2.0")) diff --git a/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/HelloResource.java b/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/HelloResource.java new file mode 100644 index 0000000000000..d5157221fb035 --- /dev/null +++ b/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/HelloResource.java @@ -0,0 +1,4 @@ +package io.quarkus.it.keycloak; + +public class HelloResource extends HelloResourceBase implements IHelloResource { +} diff --git a/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/HelloResourceBase.java b/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/HelloResourceBase.java new file mode 100644 index 0000000000000..aa7859e257b27 --- /dev/null +++ b/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/HelloResourceBase.java @@ -0,0 +1,11 @@ +package io.quarkus.it.keycloak; + +import javax.annotation.security.RolesAllowed; + +@RolesAllowed("user") +public class HelloResourceBase { + + public String hello() { + return "hello"; + } +} diff --git a/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/IHelloResource.java b/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/IHelloResource.java new file mode 100644 index 0000000000000..93b11ca35c0de --- /dev/null +++ b/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/IHelloResource.java @@ -0,0 +1,14 @@ +package io.quarkus.it.keycloak; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/hello") +public interface IHelloResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + String hello(); +} diff --git a/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/HelloResourceIT.java b/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/HelloResourceIT.java new file mode 100644 index 0000000000000..ed022787b266e --- /dev/null +++ b/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/HelloResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.keycloak; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class HelloResourceIT extends HelloResourceTest { +} diff --git a/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/HelloResourceTest.java b/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/HelloResourceTest.java new file mode 100644 index 0000000000000..da5aec4380acf --- /dev/null +++ b/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/HelloResourceTest.java @@ -0,0 +1,18 @@ +package io.quarkus.it.keycloak; + +import static io.restassured.RestAssured.when; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class HelloResourceTest { + + @Test + public void testHelloEndpoint() { + when().get("/hello") + .then() + .statusCode(401); + } +} diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PathTemplateResource.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PathTemplateResource.java new file mode 100644 index 0000000000000..323cd0119a683 --- /dev/null +++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/PathTemplateResource.java @@ -0,0 +1,13 @@ +package io.quarkus.it.opentelemetry; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +@Path("template/path/{value}") +public class PathTemplateResource { + @GET + public String get(@PathParam("value") String value) { + return "Received: " + value; + } +} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java index 70f3ccfdd5162..9d203cf3c13f3 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTestCase.java @@ -708,6 +708,43 @@ void testAsyncClientTracing() { Assertions.assertTrue(clientServerFound); } + @Test + void testTemplatedPathOnClass() { + resetExporter(); + + given() + .contentType("application/json") + .when().get("/template/path/something") + .then() + .statusCode(200) + .body(containsString("Received: something")); + + Awaitility.await().atMost(Duration.ofMinutes(2)).until(() -> getSpans().size() == 1); + Map spanData = getSpans().get(0); + Assertions.assertNotNull(spanData); + Assertions.assertNotNull(spanData.get("spanId")); + + verifyResource(spanData); + + Assertions.assertEquals("template/path/{value}", spanData.get("name")); + Assertions.assertEquals(SpanKind.SERVER.toString(), spanData.get("kind")); + Assertions.assertTrue((Boolean) spanData.get("ended")); + + Assertions.assertEquals(SpanId.getInvalid(), spanData.get("parent_spanId")); + Assertions.assertEquals(TraceId.getInvalid(), spanData.get("parent_traceId")); + Assertions.assertFalse((Boolean) spanData.get("parent_valid")); + Assertions.assertFalse((Boolean) spanData.get("parent_remote")); + + Assertions.assertEquals("GET", spanData.get("attr_http.method")); + Assertions.assertEquals("1.1", spanData.get("attr_http.flavor")); + Assertions.assertEquals("/template/path/something", spanData.get("attr_http.target")); + Assertions.assertEquals(directUrl.getAuthority(), spanData.get("attr_http.host")); + Assertions.assertEquals("http", spanData.get("attr_http.scheme")); + Assertions.assertEquals("200", spanData.get("attr_http.status_code")); + Assertions.assertNotNull(spanData.get("attr_http.client_ip")); + Assertions.assertNotNull(spanData.get("attr_http.user_agent")); + } + private void verifyResource(Map spanData) { Assertions.assertEquals("opentelemetry-integration-test", spanData.get("resource_service.name")); Assertions.assertEquals("999-SNAPSHOT", spanData.get("resource_service.version")); diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index a14eb85810d84..4d71601c206df 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -33,6 +33,7 @@ class-transformer shared-library hibernate-validator + hibernate-validator-resteasy-reactive common-jpa-entities infinispan-client devtools diff --git a/integration-tests/resteasy-reactive-rest-client/pom.xml b/integration-tests/resteasy-reactive-rest-client/pom.xml index 2d8b52256bea0..00b80a5053e23 100644 --- a/integration-tests/resteasy-reactive-rest-client/pom.xml +++ b/integration-tests/resteasy-reactive-rest-client/pom.xml @@ -31,6 +31,11 @@ io.quarkus quarkus-opentelemetry
+ + + io.quarkus + quarkus-opentelemetry-exporter-otlp + @@ -99,6 +104,19 @@ + + io.quarkus + quarkus-opentelemetry-exporter-otlp-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ConfigurableExceptionMapper.java b/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ConfigurableExceptionMapper.java index 8a1e5132fd993..ff1f0f1125489 100644 --- a/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ConfigurableExceptionMapper.java +++ b/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ConfigurableExceptionMapper.java @@ -13,15 +13,13 @@ public class ConfigurableExceptionMapper @Inject @ConfigProperty(name = "exception.message") String message; - @Inject - ExceptionConfig exceptionConfig; + + public ConfigurableExceptionMapper() { + System.out.println("ConfigurableExceptionMapper.ConfigurableExceptionMapper"); + } @Override public Response toResponse(final ConfigurableExceptionMapperException exception) { - if (!message.equals(exceptionConfig.message())) { - return Response.serverError().build(); - } - return Response.ok().entity(message).build(); } diff --git a/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ExceptionConfig.java b/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ExceptionConfig.java deleted file mode 100644 index 669464b2504c9..0000000000000 --- a/integration-tests/smallrye-config/src/main/java/io/quarkus/it/smallrye/config/ExceptionConfig.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.quarkus.it.smallrye.config; - -import io.smallrye.config.ConfigMapping; - -@ConfigMapping(prefix = "exception") -public interface ExceptionConfig { - String message(); -} diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java index 8d28ccae9e6d3..463cc04bb00c6 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java @@ -572,8 +572,9 @@ private Throwable unwrapException(Throwable cause) { public void afterAll(ExtensionContext extensionContext) throws Exception { actualTestClass = null; actualTestInstance = null; + List records = null; if (assertLogRecords != null) { - assertLogRecords.accept(inMemoryLogHandler.records); + records = new ArrayList<>(inMemoryLogHandler.records); } rootLogger.setHandlers(originalHandlers); inMemoryLogHandler.clearRecords(); @@ -606,9 +607,12 @@ public void afterAll(ExtensionContext extensionContext) throws Exception { if (afterAllCustomizer != null) { afterAllCustomizer.run(); } + ClearCache.clearAnnotationCache(); + GroovyCacheCleaner.clearGroovyCache(); + } + if (records != null) { + assertLogRecords.accept(records); } - ClearCache.clearAnnotationCache(); - GroovyCacheCleaner.clearGroovyCache(); } @Override 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 a6175dce4e58e..798048d7b9627 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 @@ -22,7 +22,6 @@ import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; -import java.util.IdentityHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -197,8 +196,6 @@ public void run() { } }; - private final IdentityHashMap tcclMethodCache = new IdentityHashMap<>(); - static { ClassLoader classLoader = QuarkusTestExtension.class.getClassLoader(); if (classLoader instanceof QuarkusClassLoader) { @@ -432,17 +429,8 @@ public void close() throws IOException { } tm.close(); } finally { - tcclMethodCache.clear(); GroovyCacheCleaner.clearGroovyCache(); - if (hangTaskKey != null) { - hangTaskKey.cancel(true); - hangTaskKey = null; - } - var h = hangDetectionExecutor; - if (h != null) { - h.shutdownNow(); - hangDetectionExecutor = null; - } + shutdownHangDetection(); } } try { @@ -471,6 +459,18 @@ public void close() throws IOException { } } + private void shutdownHangDetection() { + if (hangTaskKey != null) { + hangTaskKey.cancel(true); + hangTaskKey = null; + } + var h = hangDetectionExecutor; + if (h != null) { + h.shutdownNow(); + hangDetectionExecutor = null; + } + } + private void populateDeepCloneField(StartupAction startupAction) { deepClone = new SerializationWithXStreamFallbackDeepClone(startupAction.getClassLoader()); } @@ -1038,9 +1038,11 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation cloneRequired = false; } else if (TestInfo.class.isAssignableFrom(theclass)) { TestInfo info = (TestInfo) arg; - Method newTestMethod = determineTCCLExtensionMethod(info.getTestMethod().get(), testClassFromTCCL); + Method newTestMethod = info.getTestMethod().isPresent() + ? determineTCCLExtensionMethod(info.getTestMethod().get(), testClassFromTCCL) + : null; replacement = new TestInfoImpl(info.getDisplayName(), info.getTags(), Optional.of(testClassFromTCCL), - Optional.of(newTestMethod)); + Optional.ofNullable(newTestMethod)); } else if (clonePattern.matcher(className).matches()) { cloneRequired = true; } else { @@ -1095,10 +1097,8 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation private Method determineTCCLExtensionMethod(Method originalMethod, Class c) throws ClassNotFoundException { - Method newMethod = tcclMethodCache.get(originalMethod); - if (newMethod != null) { - return newMethod; - } + + Method newMethod = null; while (c != Object.class) { if (c.getName().equals(originalMethod.getDeclaringClass().getName())) { try { @@ -1122,7 +1122,6 @@ private Method determineTCCLExtensionMethod(Method originalMethod, Class c) } c = c.getSuperclass(); } - tcclMethodCache.put(originalMethod, newMethod); return newMethod; } @@ -1330,6 +1329,7 @@ class FailedCleanup implements ExtensionContext.Store.CloseableResource { @Override public void close() { + resetHangTimeout(); firstException = null; failedBoot = false; ConfigProviderResolver.setInstance(null);