diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 7c6d8e9a04dc8..c203fada3b809 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -39,7 +39,7 @@ 2.0 1.2 1.0 - 1.13.1 + 1.13.2 2.12.1 3.3.0 3.0.5 @@ -201,7 +201,7 @@ 2.2 2.6 - 0.10.0 + 0.11.0 9.25.6 0.0.6 0.1.1 @@ -6156,6 +6156,7 @@ io.quarkus.maven.javax.managed io.quarkus.maven.javax.versions io.quarkus.jakarta-versions + io.quarkus.jakarta-el io.quarkus.jakarta-jaxrs-jaxb io.quarkus.jakarta-security io.quarkus.smallrye diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 0f89a78fe3976..de90d71b6ab25 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -38,11 +38,6 @@ 1.0.0 - - 22.2.0 - 22.2 - 2.5.7 2.40.0 3.24.2 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java index b93c2a21ab98f..204461779cb68 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java @@ -1,30 +1,35 @@ package io.quarkus.deployment; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.ServiceLoader; +import java.util.StringJoiner; import java.util.function.Consumer; import org.eclipse.microprofile.config.Config; -import io.quarkus.bootstrap.classloading.ClassPathElement; -import io.quarkus.bootstrap.classloading.FilteredClassPathElement; +import io.quarkus.bootstrap.classloading.MemoryClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.prebuild.CodeGenException; import io.quarkus.deployment.codegen.CodeGenData; import io.quarkus.deployment.dev.DevModeContext; import io.quarkus.deployment.dev.DevModeContext.ModuleInfo; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.OpenPathTree; import io.quarkus.paths.PathCollection; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ConfigUtils; +import io.quarkus.runtime.util.ClassPathUtils; import io.smallrye.config.KeyMap; import io.smallrye.config.KeyMapBackedConfigSource; import io.smallrye.config.NameIterator; @@ -37,7 +42,9 @@ */ public class CodeGenerator { - private static final String MP_CONFIG_SPI_CONFIG_SOURCE_PROVIDER = "META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider"; + private static final List CONFIG_SOURCE_FACTORY_INTERFACES = List.of( + "META-INF/services/io.smallrye.config.ConfigSourceFactory", + "META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider"); // used by Gradle and Maven public static void initAndRun(QuarkusClassLoader classLoader, @@ -113,6 +120,7 @@ public static List init(ClassLoader deploymentClassLoader, Collecti }); } + @SuppressWarnings("unchecked") private static List loadCodeGenProviders(ClassLoader deploymentClassLoader) throws CodeGenException { Class codeGenProviderClass; @@ -174,42 +182,94 @@ public static boolean trigger(ClassLoader deploymentClassLoader, public static Config getConfig(ApplicationModel appModel, LaunchMode launchMode, Properties buildSystemProps, QuarkusClassLoader deploymentClassLoader) throws CodeGenException { // Config instance that is returned by this method should be as close to the one built in the ExtensionLoader as possible - if (appModel.getAppArtifact().getContentTree() - .contains(MP_CONFIG_SPI_CONFIG_SOURCE_PROVIDER)) { - final List allElements = ((QuarkusClassLoader) deploymentClassLoader).getAllElements(false); - // we don't want to load config sources from the current module because they haven't been compiled yet - final QuarkusClassLoader.Builder configClBuilder = QuarkusClassLoader - .builder("CodeGenerator Config ClassLoader", QuarkusClassLoader.getSystemClassLoader(), false); - final Collection appRoots = appModel.getAppArtifact().getContentTree().getRoots(); - for (ClassPathElement e : allElements) { - if (appRoots.contains(e.getRoot())) { - configClBuilder.addElement(new FilteredClassPathElement(e, List.of(MP_CONFIG_SPI_CONFIG_SOURCE_PROVIDER))); + final Map> appModuleConfigFactories = getConfigSourceFactoryImpl(appModel.getAppArtifact()); + if (!appModuleConfigFactories.isEmpty()) { + final Map> allConfigFactories = new HashMap<>(appModuleConfigFactories.size()); + final Map allowedConfigFactories = new HashMap<>(appModuleConfigFactories.size()); + final Map bannedConfigFactories = new HashMap<>(appModuleConfigFactories.size()); + for (Map.Entry> appModuleFactories : appModuleConfigFactories.entrySet()) { + final String factoryImpl = appModuleFactories.getKey(); + try { + ClassPathUtils.consumeAsPaths(deploymentClassLoader, factoryImpl, p -> { + try { + allConfigFactories.computeIfAbsent(factoryImpl, k -> new ArrayList<>()) + .addAll(Files.readAllLines(p)); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read " + p, e); + } + }); + } catch (IOException e) { + throw new CodeGenException("Failed to read resources from classpath", e); + } + final List allFactories = allConfigFactories.getOrDefault(factoryImpl, List.of()); + allFactories.removeAll(appModuleFactories.getValue()); + if (allFactories.isEmpty()) { + bannedConfigFactories.put(factoryImpl, new byte[0]); } else { - configClBuilder.addElement(e); + final StringJoiner joiner = new StringJoiner(System.lineSeparator()); + allFactories.forEach(f -> joiner.add(f)); + allowedConfigFactories.put(factoryImpl, joiner.toString().getBytes()); } } + + // we don't want to load config source factories/providers from the current module because they haven't been compiled yet + QuarkusClassLoader.Builder configClBuilder = QuarkusClassLoader.builder("CodeGenerator Config ClassLoader", + deploymentClassLoader, false); + if (!allowedConfigFactories.isEmpty()) { + configClBuilder.addElement(new MemoryClassPathElement(allowedConfigFactories, true)); + } + if (!bannedConfigFactories.isEmpty()) { + configClBuilder.addBannedElement(new MemoryClassPathElement(bannedConfigFactories, true)); + } deploymentClassLoader = configClBuilder.build(); } - final SmallRyeConfigBuilder builder = ConfigUtils.configBuilder(false, launchMode) - .forClassLoader(deploymentClassLoader); - final PropertiesConfigSource pcs = new PropertiesConfigSource(buildSystemProps, "Build system"); - final SysPropConfigSource spcs = new SysPropConfigSource(); - - final Map platformProperties = appModel.getPlatformProperties(); - if (platformProperties.isEmpty()) { - builder.withSources(pcs, spcs); - } else { - final KeyMap props = new KeyMap<>(platformProperties.size()); - for (Map.Entry prop : platformProperties.entrySet()) { - props.findOrAdd(new NameIterator(prop.getKey())).putRootValue(prop.getValue()); + try { + final SmallRyeConfigBuilder builder = ConfigUtils.configBuilder(false, launchMode) + .forClassLoader(deploymentClassLoader); + final PropertiesConfigSource pcs = new PropertiesConfigSource(buildSystemProps, "Build system"); + final SysPropConfigSource spcs = new SysPropConfigSource(); + + final Map platformProperties = appModel.getPlatformProperties(); + if (platformProperties.isEmpty()) { + builder.withSources(pcs, spcs); + } else { + final KeyMap props = new KeyMap<>(platformProperties.size()); + for (Map.Entry prop : platformProperties.entrySet()) { + props.findOrAdd(new NameIterator(prop.getKey())).putRootValue(prop.getValue()); + } + final KeyMapBackedConfigSource platformConfigSource = new KeyMapBackedConfigSource("Quarkus platform", + // Our default value configuration source is using an ordinal of Integer.MIN_VALUE + // (see io.quarkus.deployment.configuration.DefaultValuesConfigurationSource) + Integer.MIN_VALUE + 1000, props); + builder.withSources(platformConfigSource, pcs, spcs); } - final KeyMapBackedConfigSource platformConfigSource = new KeyMapBackedConfigSource("Quarkus platform", - // Our default value configuration source is using an ordinal of Integer.MIN_VALUE - // (see io.quarkus.deployment.configuration.DefaultValuesConfigurationSource) - Integer.MIN_VALUE + 1000, props); - builder.withSources(platformConfigSource, pcs, spcs); + return builder.build(); + } finally { + if (!appModuleConfigFactories.isEmpty()) { + deploymentClassLoader.close(); + } + } + } + + private static Map> getConfigSourceFactoryImpl(ResolvedDependency dep) throws CodeGenException { + final Map> configFactoryImpl = new HashMap<>(CONFIG_SOURCE_FACTORY_INTERFACES.size()); + try (OpenPathTree openTree = dep.getContentTree().open()) { + for (String s : CONFIG_SOURCE_FACTORY_INTERFACES) { + openTree.accept(s, v -> { + if (v == null) { + return; + } + try { + configFactoryImpl.put(s, Files.readAllLines(v.getPath())); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read " + v.getPath(), e); + } + }); + } + } catch (IOException e) { + throw new CodeGenException("Failed to read " + dep.getResolvedPaths(), e); } - return builder.build(); + return configFactoryImpl; } private static Path codeGenOutDir(Path generatedSourcesDir, diff --git a/devtools/cli/src/main/java/io/quarkus/cli/create/TargetGAVGroup.java b/devtools/cli/src/main/java/io/quarkus/cli/create/TargetGAVGroup.java index 5c45c4a64254a..04d083d811f8d 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/create/TargetGAVGroup.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/create/TargetGAVGroup.java @@ -1,9 +1,15 @@ package io.quarkus.cli.create; +import java.util.regex.Pattern; + import io.quarkus.devtools.commands.CreateProjectHelper; import picocli.CommandLine; +import picocli.CommandLine.TypeConversionException; public class TargetGAVGroup { + static final String BAD_IDENTIFIER = "The specified %s identifier (%s) contains invalid characters. Valid characters are alphanumeric (A-Za-z), underscore, dash and dot."; + static final Pattern OK_ID = Pattern.compile("[0-9A-Za-z_.-]+"); + static final String DEFAULT_GAV = CreateProjectHelper.DEFAULT_GROUP_ID + ":" + CreateProjectHelper.DEFAULT_ARTIFACT_ID + ":" + CreateProjectHelper.DEFAULT_VERSION; @@ -52,6 +58,13 @@ void projectGav() { } } } + if (artifactId != CreateProjectHelper.DEFAULT_ARTIFACT_ID && !OK_ID.matcher(artifactId).matches()) { + throw new TypeConversionException(String.format(BAD_IDENTIFIER, "artifactId", artifactId)); + } + if (groupId != CreateProjectHelper.DEFAULT_GROUP_ID && !OK_ID.matcher(groupId).matches()) { + throw new TypeConversionException(String.format(BAD_IDENTIFIER, "groupId", groupId)); + } + initialized = true; } } diff --git a/devtools/cli/src/test/java/io/quarkus/cli/create/TargetGAVGroupTest.java b/devtools/cli/src/test/java/io/quarkus/cli/create/TargetGAVGroupTest.java index a8714b43e238b..1602c0d7ac006 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/create/TargetGAVGroupTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/create/TargetGAVGroupTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import io.quarkus.devtools.commands.CreateProjectHelper; +import picocli.CommandLine.TypeConversionException; public class TargetGAVGroupTest { @@ -92,4 +93,16 @@ void testOldParameters() { Assertions.assertEquals("a", gav.getArtifactId()); Assertions.assertEquals("v", gav.getVersion()); } + + @Test + void testBadArtifactId() { + gav.gav = "g:a/x:v"; + Assertions.assertThrows(TypeConversionException.class, () -> gav.getArtifactId()); + } + + @Test + void testBadGroupId() { + gav.gav = "g,x:a:v"; + Assertions.assertThrows(TypeConversionException.class, () -> gav.getGroupId()); + } } 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 44a5e3f82165f..21932062a482c 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/CreateProjectMojo.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Set; import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apache.maven.execution.MavenSession; @@ -55,6 +56,8 @@ */ @Mojo(name = "create", requiresProject = false) public class CreateProjectMojo extends AbstractMojo { + static final String BAD_IDENTIFIER = "The specified %s identifier (%s) contains invalid characters. Valid characters are alphanumeric (A-Za-z), underscore, dash and dot."; + static final Pattern OK_ID = Pattern.compile("[0-9A-Za-z_.-]+"); private static final String DEFAULT_GROUP_ID = "org.acme"; private static final String DEFAULT_ARTIFACT_ID = "code-with-quarkus"; @@ -262,6 +265,13 @@ public void execute() throws MojoExecutionException { } askTheUserForMissingValues(); + if (projectArtifactId != DEFAULT_ARTIFACT_ID && !OK_ID.matcher(projectArtifactId).matches()) { + throw new MojoExecutionException(String.format(BAD_IDENTIFIER, "artifactId", projectArtifactId)); + } + if (projectGroupId != DEFAULT_GROUP_ID && !OK_ID.matcher(projectGroupId).matches()) { + throw new MojoExecutionException(String.format(BAD_IDENTIFIER, "groupId", projectGroupId)); + } + projectRoot = new File(outputDirectory, projectArtifactId); if (projectRoot.exists()) { throw new MojoExecutionException("Unable to create the project, " + diff --git a/docs/pom.xml b/docs/pom.xml index ba48af4f52e47..be85311b46bcf 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -15,6 +15,11 @@ jar + + 22.2 + 22.2 + 2.0.0 1.5.0-beta.8 2.26.0.Final diff --git a/docs/src/main/asciidoc/_attributes.adoc b/docs/src/main/asciidoc/_attributes.adoc index 68551adfe147e..4af0060ec1754 100644 --- a/docs/src/main/asciidoc/_attributes.adoc +++ b/docs/src/main/asciidoc/_attributes.adoc @@ -3,9 +3,9 @@ :maven-version: ${proposed-maven-version} :graalvm-version: ${graal-sdk.version-for-documentation} -:graalvm-flavor: ${graal-sdk.version-for-documentation}-java11 +:graalvm-flavor: ${graal-sdk.version-for-documentation}-java17 :mandrel-version: ${mandrel.version-for-documentation} -:mandrel-flavor: ${mandrel.version-for-documentation}-java11 +:mandrel-flavor: ${mandrel.version-for-documentation}-java17 :surefire-version: ${version.surefire.plugin} :gradle-version: ${gradle-wrapper.version} :elasticsearch-version: ${elasticsearch-server.version} diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index ef223f510150c..b1e2a9ef71425 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -233,7 +233,7 @@ You can do so by prepending the flag with `-J` and passing it as additional nati IMPORTANT: Fully static native executables support is experimental. On Linux it's possible to package a native executable that doesn't depend on any system shared library. -There are https://www.graalvm.org/22.1/reference-manual/native-image/StaticImages/#preparation[some system requirements] to be fulfilled and additional build arguments to be used along with the `native-image` invocation, a minimum is `-Dquarkus.native.additional-build-args="--static","--libc=musl"`. +There are https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/guides/build-static-executables/#prerequisites-and-preparation[some system requirements] to be fulfilled and additional build arguments to be used along with the `native-image` invocation, a minimum is `-Dquarkus.native.additional-build-args="--static","--libc=musl"`. Compiling fully static binaries is done by statically linking https://musl.libc.org/[musl] instead of `glibc` and should not be used in production without rigorous testing. diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index 72b122ddb8643..f2f2087b72656 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -158,7 +158,7 @@ quarkus.arc.exclude-dependency.acme.artifact-id=acme-services <2> == Native Executables and Private Members Quarkus is using GraalVM to build a native executable. -One of the limitations of GraalVM is the usage of https://www.graalvm.org/22.2/reference-manual/native-image/Reflection/[Reflection, window="_blank"]. +One of the limitations of GraalVM is the usage of https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/Reflection/[Reflection, window="_blank"]. Reflective operations are supported but all relevant members must be registered for reflection explicitly. Those registrations result in a bigger native executable. diff --git a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc index 1a1417fc45841..bdf62b6ff05ce 100644 --- a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc +++ b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc @@ -53,8 +53,6 @@ follow the specific guides for more information on how to develop, package and d == Deploying to Google App Engine Standard -We will only cover the Java 11 runtime as the Java 8 runtime uses its own Servlet engine which is not compatible with Quarkus. - First, make sure to have an App Engine environment initialized for your Google Cloud project, if not, initialize one via `gcloud app create --project=[YOUR_PROJECT_ID]`. Then, you will need to create a `src/main/appengine/app.yaml` file, let's keep it minimalistic with only the selected engine: diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 2ea9e3f06ce1e..00f248c1aebec 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -625,7 +625,7 @@ containers: memory: 64Mi ---- -=== Exposing with Secured Ingress +=== Exposing your application in Kubernetes Kubernetes exposes applications using https://kubernetes.io/docs/concepts/services-networking/ingress[Ingress resources]. To generate the Ingress resource, just apply the following configuration: @@ -661,7 +661,109 @@ spec: pathType: Prefix ---- -After deploying these resources to Kubernetes, the Ingress resource will allow unsecured connections to reach out your application. +After deploying these resources to Kubernetes, the Ingress resource will allow unsecured connections to reach out your application. + +==== Adding Ingress rules + +To customize the default `host` and `path` properties of the generated Ingress resources, you need to apply the following configuration: + +[source] +---- +quarkus.kubernetes.ingress.expose=true +# To change the Ingress host. By default, it's empty. +quarkus.kubernetes.ingress.host=prod.svc.url +# To change the Ingress path of the generated Ingress rule. By default, it's "/". +quarkus.kubernetes.ports.http.path=/prod +---- + +This would generate the following Ingress resource: + +[source, yaml] +---- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + labels: + app.kubernetes.io/name: kubernetes-with-ingress + app.kubernetes.io/version: 0.1-SNAPSHOT + name: kubernetes-with-ingress +spec: + rules: + - host: prod.svc.url + http: + paths: + - backend: + service: + name: kubernetes-with-ingress + port: + name: http + path: /prod + pathType: Prefix +---- + +Additionally, you can also add new Ingress rules by adding the following configuration: + +[source] +---- +# Example to add a new rule +quarkus.kubernetes.ingress.rules.1.host=dev.svc.url +quarkus.kubernetes.ingress.rules.1.path=/dev +quarkus.kubernetes.ingress.rules.1.path-type=ImplementationSpecific +# by default, path type is Prefix + +# Exmple to add a new rule that use another service binding +quarkus.kubernetes.ingress.rules.2.host=alt.svc.url +quarkus.kubernetes.ingress.rules.2.path=/ea +quarkus.kubernetes.ingress.rules.2.service-name=updated-service +quarkus.kubernetes.ingress.rules.2.service-port-name=tcpurl +---- + +This would generate the following Ingress resource: + +[source, yaml] +---- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + labels: + app.kubernetes.io/name: kubernetes-with-ingress + app.kubernetes.io/version: 0.1-SNAPSHOT + name: kubernetes-with-ingress +spec: + rules: + - host: prod.svc.url + http: + paths: + - backend: + service: + name: kubernetes-with-ingress + port: + name: http + path: /prod + pathType: Prefix + - host: dev.svc.url + http: + paths: + - backend: + service: + name: kubernetes-with-ingress + port: + name: http + path: /dev + pathType: ImplementationSpecific + - host: alt.svc.url + http: + paths: + - backend: + service: + name: updated-service + port: + name: tcpurl + path: /ea + pathType: Prefix +---- + +==== Securing the Ingress resource To secure the incoming connections, Kubernetes allows enabling https://kubernetes.io/docs/concepts/services-networking/ingress/#tls[TLS] within the Ingress resource by specifying a Secret that contains a TLS private key and certificate. You can generate a secured Ingress resource by simply adding the "tls.secret-name" properties: @@ -1218,6 +1320,12 @@ To enable Service Binding for supported extensions, add the `quarkus-kubernetes- * `quarkus-kafka-client` * `quarkus-smallrye-reactive-messaging-kafka` + +* `quarkus-reactive-db2-client` +* `quarkus-reactive-mssql-client` +* `quarkus-reactive-mysql-client` +* `quarkus-reactive-oracle-client` +* `quarkus-reactive-pg-client` ==== diff --git a/docs/src/main/asciidoc/doc-reference.adoc b/docs/src/main/asciidoc/doc-reference.adoc index cca2b5737ac5b..4bee5f0ee4b98 100644 --- a/docs/src/main/asciidoc/doc-reference.adoc +++ b/docs/src/main/asciidoc/doc-reference.adoc @@ -324,6 +324,6 @@ The complete list of externalized variables for use is given in the following ta |\{quickstarts-tree-url}|{quickstarts-tree-url}| Quickstarts URL to main source tree root; used for referencing directories. |\{graalvm-version}|{graalvm-version}| Recommended GraalVM version to use. -|\{graalvm-flavor}|{graalvm-flavor}| The full version of GraalVM to use e.g. `19.3.1-java11`. Use a `java11` version. +|\{graalvm-flavor}|{graalvm-flavor}| The builder image tag of GraalVM to use e.g. `22.2-java17`. Use a `java17` version. |=== diff --git a/docs/src/main/asciidoc/gradle-tooling.adoc b/docs/src/main/asciidoc/gradle-tooling.adoc index 857fcf795c961..a41da1d4d991f 100644 --- a/docs/src/main/asciidoc/gradle-tooling.adoc +++ b/docs/src/main/asciidoc/gradle-tooling.adoc @@ -385,7 +385,7 @@ Once executed, you will be able to safely run quarkus task with `--offline` flag Native executables make Quarkus applications ideal for containers and serverless workloads. -Make sure to have `GRAALVM_HOME` configured and pointing to GraalVM version {graalvm-version} (Make sure to use a Java 11 version of GraalVM). +Make sure to have `GRAALVM_HOME` configured and pointing to the latest release of GraalVM version {graalvm-version} (Make sure to use a Java 11 version of GraalVM). Create a native executable using: diff --git a/docs/src/main/asciidoc/kotlin.adoc b/docs/src/main/asciidoc/kotlin.adoc index 7de81548e2770..cde351f316dde 100644 --- a/docs/src/main/asciidoc/kotlin.adoc +++ b/docs/src/main/asciidoc/kotlin.adoc @@ -16,12 +16,6 @@ Quarkus provides first class support for using Kotlin as will be explained in th include::{includes}/prerequisites.adoc[] -[WARNING] -==== -If building with Mandrel, make sure to use version Mandrel 22.1 or above, for example `ubi-quarkus-mandrel-builder-image:{mandrel-flavor}`. -With older versions, you might encounter errors when trying to deserialize JSON documents that have null or missing fields, similar to the errors mentioned in the <> section. -==== - NB: For Gradle project setup please see below, and for further reference consult the guide in the xref:gradle-tooling.adoc[Gradle setup page]. == Creating the Maven project diff --git a/docs/src/main/asciidoc/maven-tooling.adoc b/docs/src/main/asciidoc/maven-tooling.adoc index 1c48c6b211053..9aac9d4771223 100644 --- a/docs/src/main/asciidoc/maven-tooling.adoc +++ b/docs/src/main/asciidoc/maven-tooling.adoc @@ -374,7 +374,7 @@ This goal will resolve all the runtime, build time, test and dev mode dependenci Native executables make Quarkus applications ideal for containers and serverless workloads. -Make sure to have `GRAALVM_HOME` configured and pointing to GraalVM version {graalvm-version} (Make sure to use a Java 11 version of GraalVM). +Make sure to have `GRAALVM_HOME` configured and pointing to the latest release of GraalVM version {graalvm-version}. Verify that your `pom.xml` has the proper `native` profile (see <>). Create a native executable using: diff --git a/docs/src/main/asciidoc/native-and-ssl.adoc b/docs/src/main/asciidoc/native-and-ssl.adoc index 7b126d560f2f0..c622261dc511e 100644 --- a/docs/src/main/asciidoc/native-and-ssl.adoc +++ b/docs/src/main/asciidoc/native-and-ssl.adoc @@ -22,7 +22,7 @@ To complete this guide, you need: * less than 20 minutes * an IDE -* GraalVM (Java 11) installed with `JAVA_HOME` and `GRAALVM_HOME` configured appropriately +* GraalVM installed with `JAVA_HOME` and `GRAALVM_HOME` configured appropriately * Apache Maven {maven-version} This guide is based on the REST client guide, so you should get this Maven project first. @@ -253,7 +253,7 @@ The file containing the custom TrustStore does *not* (and probably should not) h === Run time configuration -Using the runtime certificate configuration, supported by GraalVM since 21.3 does not require any special or additional configuration compared to regular java programs or Quarkus in jvm mode. See the https://www.graalvm.org/22.2/reference-manual/native-image/CertificateManagement/#run-time-options[GraalVM documentation] for more information. +Using the runtime certificate configuration, supported by GraalVM since 21.3 does not require any special or additional configuration compared to regular java programs or Quarkus in jvm mode. See the https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/dynamic-features/CertificateManagement/#runtime-options[GraalVM documentation] for more information. [#working-with-containers] === Working with containers diff --git a/docs/src/main/asciidoc/native-reference.adoc b/docs/src/main/asciidoc/native-reference.adoc index 5a5ab765b33bc..825609e20f156 100644 --- a/docs/src/main/asciidoc/native-reference.adoc +++ b/docs/src/main/asciidoc/native-reference.adoc @@ -12,14 +12,157 @@ xref:building-native-image.adoc[Building a Native Executable], xref:native-and-ssl.adoc[Using SSL With Native Images], and xref:writing-native-applications-tips.adoc[Writing Native Applications], guides. -It provides further details to debugging issues in Quarkus native executables that might arise during development or production. +It explores advanced topics that help users diagnose issues, +increase the reliability and improve the runtime performance of native executables. +These are the high level sections to be found in this guide: + +* <> +* <> +* <> + +[[native-memory-management]] +== Native Memory Management +Memory management for Quarkus native executables is enabled by GraalVM’s SubstrateVM runtime system. +The memory management component in GraalVM is explained in detail +link:https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/optimizations-and-performance/MemoryManagement[here]. +This guide complements the information available in the GraalVM website with further observations particularly relevant to Quarkus applications. + +=== Garbage Collectors +The garbage collectors available for Quarkus users are currently Serial GC and Epsilon GC. + +==== Serial GC +Serial GC, the default option in GraalVM and Quarkus, is a single-threaded non-concurrent GC, just like HotSpot’s Serial GC. +The implementation in GraalVM however is different from the HotSpot one, +and there can be significant differences in the runtime behavior. + +One of the key differences between HotSpot’s Serial GC and GraalVM’s Serial GC is the way they perform full GC cycles. +In HotSpot the algorithm used is mark-sweep-compact whereas in GraalVM it is mark-copy. +Both need to traverse all live objects, +but in mark-copy this traversal is also used to copy live objects to a secondary space or semi-space. +As objects are copied from one semi-space to another they’re also compacted. +In mark-sweep-compact, the compacting requires a second pass on the live objects. +This makes full GCs in mark-copy more time efficient (in terms of time spent in each GC cycle) than mark-sweep-compact. +The tradeoff mark-copy makes in order to make individual full GC cycles shorter is space. +The use of semi-spaces means that for an application to maintain the same GC performance that mark-sweep achieves (in terms of allocated MB per second), +it requires double the amount of memory. + +===== GC Collection Policy + +GraalVM's Serial GC implementation offers a choice between two different collection policies, the default is called "adaptive" and the alternative is called "space/time". + +The “adaptive” collection policy is based on HotSpot's ParallelGC adaptive size policy. +The main difference with HotSpot is GraalVM's focus on memory footprint. +This means that GraalVM’s adaptive GC policy tries to aggressively trigger GCs in order to keep memory consumption down. + +Up to version 2.13, Quarkus used the “space/time” GC collection policy by default, +but starting with version 2.14, it switched to using the “adaptive” policy instead. +The reason why Quarkus initially chose to use "space/time" is because at that time it had considerable performance improvements over "adaptive". +Recent performance experiments, however, indicate that the "space/time" policy can result in worse out-of-the-box experience compared to the "space/time" policy, +while at the same time the benefits it used to offer have diminished considerably after improvements made to the "adaptive" policy. +As a result, the "adaptive" policy appears to be the best option for most, if not all, Quarkus applications. +Full details on this switch can be read in link:https://github.com/quarkusio/quarkus/issues/28267[this issue]. + +It is still possible to change the GC collection policy using GraalVM’s `-H:InitialCollectionPolicy` flag. +Switching to the "space/time" policy can be done by passing the following via command line: -This reference guide takes as input the application developed in the xref:getting-started.adoc[Getting Started Guide]. +[source,bash] +---- +-Dquarkus.native.additional-build-args=-H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy\$BySpaceAndTime +---- + +Or adding this to the `application.properties` file: + +[source,properties] +---- +quarkus.native.additional-build-args=-H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime +---- + +[NOTE] +==== +Escaping the `$` character is required to configure the "space/time" GC collection policy if passing via command line in Bash. +Other command line environments might have similar requirements. +==== + +==== Epsilon GC +Epsilon GC is a no-op garbage collector which does not do any memory reclamation. +From a Quarkus perspective, some of the most relevant use cases for this garbage collector are extremely short-lived jobs, e.g. serverless functions. +To build Quarkus native with epsilon GC, pass the following argument at build time: + +[source,bash] +---- +-Dquarkus.native.additional-build-args=--gc=epsilon +---- + +=== Memory Management Options +Options to control maximum heap size, young space and other typical use cases found in the JVM can be found in +https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/optimizations-and-performance/MemoryManagement[the GraalVM memory management guide]. +Setting the maximum heap size, either as a percentage or an explicit value, is generally recommended. + +[[gc-logging]] +=== GC Logging +Multiple options exist to print information about garbage collection cycles, depending on the level of detail required. +The minimum detail is provided `-XX:+PrintGC`, which prints a message for each GC cycle that occurs: + +[source,bash] +---- +$ quarkus-project-0.1-SNAPSHOT-runner -XX:+PrintGC -Xmx64m +... +[Incremental GC (CollectOnAllocation) 20480K->11264K, 0.0003223 secs] +[Full GC (CollectOnAllocation) 19456K->5120K, 0.0031067 secs] +---- + +When you combine this option with `-XX:+VerboseGC` you still get a message per GC cycle, +but it contains extra information. +Also, adding this option shows the sizing decisions made by the GC algorithm at startup: + +[source,bash] +---- +$ quarkus-project-0.1-SNAPSHOT-runner -XX:+PrintGC -XX:+VerboseGC -Xmx64m +[Heap policy parameters: +YoungGenerationSize: 25165824 +MaximumHeapSize: 67108864 +MinimumHeapSize: 33554432 +AlignedChunkSize: 1048576 +LargeArrayThreshold: 131072] +... +[[5378479783321 GC: before epoch: 8 cause: CollectOnAllocation] +[Incremental GC (CollectOnAllocation) 16384K->9216K, 0.0003847 secs] +[5378480179046 GC: after epoch: 8 cause: CollectOnAllocation policy: adaptive type: incremental +collection time: 384755 nanoSeconds]] +[[5379294042918 GC: before epoch: 9 cause: CollectOnAllocation] +[Full GC (CollectOnAllocation) 17408K->5120K, 0.0030556 secs] +[5379297109195 GC: after epoch: 9 cause: CollectOnAllocation policy: adaptive type: complete +collection time: 3055697 nanoSeconds]] +---- + +Beyond these two options, `-XX:+PrintHeapShape` and `-XX:+TraceHeapChunks` provide even lower level details about memory chunks on top of which the different memory regions are constructed. + +The most up-to-date information on GC logging flags can be obtained by printing the list of flags that can be passed to native executables: + +[source,bash] +---- +$ quarkus-project-0.1-SNAPSHOT-runner -XX:PrintFlags= +... + -XX:±PrintGC Print summary GC information after each collection. Default: - (disabled). + -XX:±PrintGCSummary Print summary GC information after application main method returns. Default: - (disabled). + -XX:±PrintGCTimeStamps Print a time stamp at each collection, if +PrintGC or +VerboseGC. Default: - (disabled). + -XX:±PrintGCTimes Print the time for each of the phases of each collection, if +VerboseGC. Default: - (disabled). + -XX:±PrintHeapShape Print the shape of the heap before and after each collection, if +VerboseGC. Default: - (disabled). +... + -XX:±TraceHeapChunks Trace heap chunks during collections, if +VerboseGC and +PrintHeapShape. Default: - (disabled). + -XX:±VerboseGC Print more information about the heap before and after each collection. Default: - (disabled). +---- + +[[inspecting-and-debugging]] +== Inspecting and Debugging Native Executables +This debugging guide provides further details on debugging issues in Quarkus native executables that might arise during development or production. + +It takes as input the application developed in the xref:getting-started.adoc[Getting Started Guide]. You can find instructions on how to quickly set up this application in this guide. -== Requirements and Assumptions +=== Requirements and Assumptions -This guide has the following requirements: +This debugging guide has the following requirements: * JDK 11 installed with `JAVA_HOME` configured appropriately * Apache Maven {maven-version} @@ -40,7 +183,7 @@ A minimum of 4 CPUs and 4GB of memory is required. Finally, this guide assumes the use of the link:https://github.com/graalvm/mandrel[Mandrel distribution] of GraalVM for building native executables, and these are built within a container so there is no need for installing Mandrel on the host. -== Bootstrapping the project +=== Bootstrapping the project Start by creating a new Quarkus project. Open a terminal and run the following command: @@ -57,9 +200,9 @@ For Windows users - If using cmd , (don't use backward slash `\` and put everything on the same line) - If using Powershell , wrap `-D` parameters in double quotes e.g. `"-DprojectArtifactId=debugging-native"` -== Configure Quarkus properties +=== Configure Quarkus properties -Some Quarkus configuration options will be used constantly throughout this guide, +Some Quarkus configuration options will be used constantly throughout this debugging guide, so to help declutter command line invocations, it's recommended to add these options to the `application.properties` file. So, go ahead and add the following options to that file: @@ -72,7 +215,7 @@ quarkus.container-image.build=true quarkus.container-image.group=test ---- -== First Debugging Steps +=== First Debugging Steps As a first step, change to the project directory and build the native executable for the application: @@ -190,7 +333,7 @@ Remember that if an argument for `-Dquarkus.native.additional-build-args` includ it needs to be escaped to be processed correctly, e.g. `\\,`. ==== -== Inspecting Native Executables +=== Inspecting Native Executables Given a native executable, various Linux tools can be used to inspect it. To allow supporting a variety of environments, @@ -276,7 +419,7 @@ From there, you can either inspect the executable directly or use a tools contai ==== [[native-reports]] -== Native Reports +=== Native Reports Optionally, the native build process can generate reports that show what goes into the binary: @@ -289,7 +432,7 @@ Optionally, the native build process can generate reports that show what goes in The reports will be created under `target/debugging-native-1.0.0-SNAPSHOT-native-image-source-jar/reports/`. These reports are some of the most useful resources when encountering issues with missing methods/classes, or encountering forbidden methods by Mandrel. -=== Call Tree Reports +==== Call Tree Reports `call_tree` csv file reports are some of the default reports generated when the `-Dquarkus.native.enable-reports` option is passed in. These csv files can be imported into a graph database, such as Neo4j, @@ -475,12 +618,12 @@ For further information, check out this link:https://quarkus.io/blog/quarkus-native-neo4j-call-tree[blog post] that explores the Quarkus Hibernate ORM quickstart using the techniques explained above. -=== Used Packages/Classes/Methods Reports +==== Used Packages/Classes/Methods Reports `used_packages`, `used_classes` and `used_methods` text file reports come in handy when comparing different versions of the application, e.g. why does the image take longer to build? Or why is the image bigger now? -=== Further Reports +==== Further Reports Mandrel can produce further reports beyond the ones that are enabled with the `-Dquarkus.native.enable-reports` option. These are called expert options and you can learn more about them by running: @@ -498,7 +641,7 @@ so they might change anytime. To use these expert options, add them comma separated to the `-Dquarkus.native.additional-build-args` parameter. -== Build-time vs Run-time Initialization +=== Build-time vs Run-time Initialization Quarkus instructs Mandrel to initialize as much as possible at build time, so that runtime startup can be as fast as possible. @@ -705,9 +848,9 @@ hellomandrel Additional information on which classes are initialized and why can be obtained by passing in the `-H:+PrintClassInitialization` flag via `-Dquarkus.native.additional-build-args`. [[profiling]] -== Profile Runtime Behaviour +=== Profile Runtime Behaviour -=== Single Thread +==== Single Thread In this exercise, we profile the runtime behaviour of some Quarkus application that was compiled to a native executable to determine where the bottleneck is. Assume that you’re in a scenario where profiling the pure Java version is not possible, maybe because the issue only occurs with the native version of the application. @@ -901,7 +1044,7 @@ The issue is that 1 million characters need to be shifted in very small incremen image::native-reference-perf-flamegraph-symbols.svg[Perf flamegraph with symbols] -=== Multi-Thread +==== Multi-Thread Multithreaded programs might require special attention when trying to understand their runtime behaviour. To demonstrate this, add this `MulticastResource` code to your project @@ -1036,7 +1179,7 @@ When you open the flamegraph, you will see all threads' work collapsed into a si Then, you can clearly see that there's some locking that could affect performance. [[debug-info]] -== Debugging Native Crashes +=== Debugging Native Crashes One of the drawbacks of using native executables is that they cannot be debugged using the standard Java debuggers, instead we need to debug them using `gdb`, the GNU Project debugger. @@ -1339,8 +1482,9 @@ We can now examine line `169` and get a first hint of what might be wrong (in this case we see that it fails at the first read from src which contains the address `0x0000`), or walk up the stack using `gdb`’s `up` command to see what part of our code led to this situation. To learn more about using gdb to debug native executables see -https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/DebugInfo.md[here]. +https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/debugging-and-diagnostics/DebugInfo/[here]. +[[native-faq]] == Frequently Asked Questions === Why is the process of generating a native executable slow? @@ -1459,26 +1603,11 @@ com.oracle.svm.core.VM=GraalVM 22.0.0.2-Final Java 11 Mandrel Distribution === How do I enable GC logging in native executables? -Executing the native executable with `-XX:PrintFlags=` prints a list of flags that can be passed to native executables. -For various levels of GC logging one may use: - -[source,bash] ----- -$ ./target/debugging-native-1.0.0-SNAPSHOT-runner -XX:PrintFlags= -... - -XX:±PrintGC Print summary GC information after each collection. Default: - (disabled). - -XX:±PrintGCSummary Print summary GC information after application main method returns. Default: - (disabled). - -XX:±PrintGCTimeStamps Print a time stamp at each collection, if +PrintGC or +VerboseGC. Default: - (disabled). - -XX:±PrintGCTimes Print the time for each of the phases of each collection, if +VerboseGC. Default: - (disabled). - -XX:±PrintHeapShape Print the shape of the heap before and after each collection, if +VerboseGC. Default: - (disabled). -... - -XX:±TraceHeapChunks Trace heap chunks during collections, if +VerboseGC and +PrintHeapShape. Default: - (disabled). - -XX:±VerboseGC Print more information about the heap before and after each collection. Default: - (disabled). ----- +See <> for details. === Can I get a heap dump of a native executable? e.g. if it runs out of memory -Starting with GraalVM 22.2.0 it will be possible to heap dumps upon request, +Starting with GraalVM 22.2.0 it is possible to create heap dumps upon request, e.g. `kill -SIGUSR1 `. Support for dumping the heap dump upon an out of memory error will follow up. @@ -1616,7 +1745,7 @@ Once the image is compiled, enable and start JFR via runtime flags: `-XX:+Flight -XX:StartFlightRecording="filename=recording.jfr" ---- -For more details on using JFR, see https://www.graalvm.org/22.2/reference-manual/native-image/debugging-and-diagnostics/JFR/[here]. +For more details on using JFR, see https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/debugging-and-diagnostics/JFR/[here]. === How can we troubleshoot performance problems only reproducible in production? diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 32e89db2129e1..b9e12fd8393b9 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1164,7 +1164,7 @@ The snippet above should render something like: ---- -TIP: In Quarkus, it is also possible to define a <> via the `@CheckedFragment` annotation. +TIP: In Quarkus, it is also possible to define a <>. You can also include a fragment with an `{#include}` section inside another template or the template that defines the fragment. @@ -1175,11 +1175,15 @@ You can also include a fragment with an `{#include}` section inside another temp

This document contains a detailed info about a user.

-{#include item[item_aliases] aliases=user.aliases /} <1><2> +{#include item$item_aliases aliases=user.aliases /} <1><2> ---- -<1> The `item[item_aliases]` parameter is translated as: _use the fragment `item_aliases` from the template `item`._ +<1> A template identifier that contains a dollar sign `$` denotes a fragment. The `item$item_aliases` value is translated as: _Use the fragment `item_aliases` from the template `item`._ <2> The `aliases` parameter is used to pass the relevant data. We need to make sure that the data are set correctly. In this particular case the fragment will use the expression `user.aliases` as the value of `aliases` in the `{#for alias in aliases}` section. +TIP: If you want to reference a fragment from the same template you can skip the part before `$`, i.e. something like `{#include $item_aliases /}`. + +NOTE: You can specify `{#include item$item_aliases _ignoreFragments=true /}` in order to disable this feature, i.e. a dollar sign `$` in the template identifier does not result in a fragment lookup. + ===== Hidden Fragments By default, a fragment is normally rendered as a part of the original template. @@ -1689,7 +1693,7 @@ public class ItemResource { ==== Type-safe Fragments You can also define a type-safe <> in your Java code. -A fragment method is annotated with `@io.quarkus.qute.CheckedFragment`. +A _native static_ method with the name that contains a dollar sign `$` denotes a method that represents a fragment of a type-safe template. The name of the fragment is derived from the annotated method name. The part before the last occurence of a dollar sign `$` is the method name of the related type-safe template. The part after the last occurence of a dollar sign is the fragment identifier. @@ -1698,7 +1702,6 @@ The strategy defined by the relevant `CheckedTemplate#defaultName()` is honored .Type-safe Fragment Example [source,java] ---- -import io.quarkus.qute.CheckedFragment; import io.quarkus.qute.CheckedTemplate; import org.acme.Item; @@ -1709,7 +1712,6 @@ class Templates { static native TemplateInstance items(List items); // defines a fragment of Templates#items() with identifier "item" - @CheckedFragment static native TemplateInstance items$item(Item item); <1> } ---- @@ -1742,6 +1744,8 @@ class ItemService { } ---- +NOTE: You can specify `@CheckedTemplate#ignoreFragments=true` in order to disable this feature, i.e. a dollar sign `$` in the method name will not result in a checked fragment method. + [[template_extension_methods]] === Template Extension Methods diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 2b2609cfeef64..ff07bba6af0a0 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -578,6 +578,35 @@ public class Endpoint { } ---- +Additionally, you can also manually append the parts of the form using the class `MultipartFormDataOutput` as: + +[source,java] +---- +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.server.core.multipart.MultipartFormDataOutput; + +@Path("multipart") +public class Endpoint { + + @GET + @Produces(MediaType.MULTIPART_FORM_DATA) + @Path("file") + public MultipartFormDataOutput getFile() { + MultipartFormDataOutput form = new MultipartFormDataOutput(); + form.addFormData("person", new Person("John"), MediaType.APPLICATION_JSON_TYPE); + form.addFormData("status", "a status", MediaType.TEXT_PLAIN_TYPE) + .getHeaders().putSingle("extra-header", "extra-value"); + return form; + } +} +---- + +This last approach allows you adding extra headers to the output part. + WARNING: For the time being, returning Multipart data is limited to be blocking endpoints. ==== Handling malformed input diff --git a/docs/src/main/asciidoc/security-getting-started.adoc b/docs/src/main/asciidoc/security-getting-started.adoc index 4d94e34199cf3..d6a4502f4c363 100644 --- a/docs/src/main/asciidoc/security-getting-started.adoc +++ b/docs/src/main/asciidoc/security-getting-started.adoc @@ -374,7 +374,7 @@ As you can see in this code sample, you do not need to start the test container [NOTE] ==== If you start your application in dev mode, `Dev Services for PostgreSQL` launches a `PostgreSQL` `devmode` container so that you can start developing your application. -While developing your application, you can also start to add tests one by one and run them by using the xref:continuous-testing.adoc[Continous Testing] feature. +While developing your application, you can also start to add tests one by one and run them by using the xref:continuous-testing.adoc[Continuous Testing] feature. `Dev Services for PostgreSQL` supports testing while you develop by providing a separate `PostgreSQL` test container that does not conflict with the `devmode` container. ==== diff --git a/docs/src/main/asciidoc/spring-data-rest.adoc b/docs/src/main/asciidoc/spring-data-rest.adoc index efabca09ab41c..09225c5361499 100644 --- a/docs/src/main/asciidoc/spring-data-rest.adoc +++ b/docs/src/main/asciidoc/spring-data-rest.adoc @@ -7,14 +7,10 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: compatibility :summary: Spring Data REST simplifies the creation of CRUD applications based on our Spring Data compatibility layer. -:extension-status: preview While users are encouraged to use REST Data with Panache for the REST data access endpoints generation, Quarkus provides a compatibility layer for Spring Data REST in the form of the `spring-data-rest` extension. - -include::{includes}/extension-status.adoc[] - == Prerequisites include::{includes}/prerequisites.adoc[] diff --git a/docs/src/main/asciidoc/writing-native-applications-tips.adoc b/docs/src/main/asciidoc/writing-native-applications-tips.adoc index 997276d1cd413..9f980abf296d9 100644 --- a/docs/src/main/asciidoc/writing-native-applications-tips.adoc +++ b/docs/src/main/asciidoc/writing-native-applications-tips.adoc @@ -76,7 +76,7 @@ Here we include all the XML files and JSON files into the native executable. [NOTE] ==== -You can find more information about this topic in https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/Resources.md[the GraalVM documentation]. +You can find more information about this topic in https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/dynamic-features/Resources/[the GraalVM documentation]. ==== The final order of business is to make the configuration file known to the `native-image` executable by adding the proper configuration to `application.properties`: @@ -245,7 +245,7 @@ As an example, in order to register all methods of class `com.acme.MyClass` for [NOTE] ==== -For more details on the format of this file, please refer to https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/Reflection.md[the GraalVM documentation]. +For more details on the format of this file, please refer to https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/dynamic-features/Reflection/[the GraalVM documentation]. ==== The final order of business is to make the configuration file known to the `native-image` executable by adding the proper configuration to `application.properties`: @@ -327,7 +327,7 @@ It should be added to the `native-image` configuration using the `quarkus.native [NOTE] ==== -You can find more information about all this in https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/ClassInitialization.md[the GraalVM documentation]. +You can find more information about all this in https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/optimizations-and-performance/ClassInitialization/[the GraalVM documentation]. ==== [NOTE] @@ -360,7 +360,7 @@ com.oracle.svm.core.jdk.UnsupportedFeatureError: Proxy class defined by interfac ---- Solving this issue requires adding the `-H:DynamicProxyConfigurationResources=` option and to provide a dynamic proxy configuration file. -You can find all the information about the format of this file in https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/DynamicProxy.md#manual-configuration[the GraalVM documentation]. +You can find all the information about the format of this file in https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/guides/configure-dynamic-proxies/[the GraalVM documentation]. [[modularity-benefits]] === Modularity Benefits @@ -612,7 +612,7 @@ public class SaxParserProcessor { [NOTE] ==== -More information about reflection in GraalVM can be found https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/Reflection.md[here]. +More information about reflection in GraalVM can be found https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/dynamic-features/Reflection/[here]. ==== === Including resources @@ -633,7 +633,7 @@ public class ResourcesProcessor { [NOTE] ==== -For more information about GraalVM resource handling in native executables please refer to https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/Resources.md[the GraalVM documentation]. +For more information about GraalVM resource handling in native executables please refer to https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/dynamic-features/Resources/[the GraalVM documentation]. ==== @@ -657,7 +657,7 @@ Using such a construct means that a `--initialize-at-run-time` option will autom [NOTE] ==== -For more information about `--initialize-at-run-time`, please read https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/ClassInitialization.md[the GraalVM documentation]. +For more information about `--initialize-at-run-time`, please read https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/optimizations-and-performance/ClassInitialization/[the GraalVM documentation]. ==== === Managing Proxy Classes @@ -681,7 +681,7 @@ Using such a construct means that a `-H:DynamicProxyConfigurationResources` opti [NOTE] ==== -For more information about Proxy Classes you can read https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/DynamicProxy.md[the GraalVM documentation]. +For more information about Proxy Classes you can read https://www.graalvm.org/{graalvm-version}/reference-manual/native-image/guides/configure-dynamic-proxies/[the GraalVM documentation]. ==== === Logging with Native Image diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index a013842b0082b..9b5840817838c 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -156,6 +156,7 @@ AdditionalBeanBuildItem quarkusApplication(CombinedIndexBuildItem combinedIndex) quarkusApplications.add(quarkusApplication.name().toString()); } } + return AdditionalBeanBuildItem.builder().setUnremovable() .setDefaultScope(DotName.createSimple(ApplicationScoped.class.getName())) .addBeanClasses(quarkusApplications) diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInterceptor.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInterceptor.java index 8a10598831305..d224a6a4e05f9 100644 --- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInterceptor.java +++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/CacheInterceptor.java @@ -62,13 +62,13 @@ public CacheInterceptionContext get() { private Optional> getArcCacheInterceptionContext( InvocationContext invocationContext, Class interceptorBindingClass) { Set bindings = InterceptorBindings.getInterceptorBindings(invocationContext); - if (bindings == null) { + if ((bindings == null) || bindings.isEmpty()) { LOGGER.trace("Interceptor bindings not found in ArC"); // This should only happen when the interception is not managed by Arc. return Optional.empty(); } - List interceptorBindings = new ArrayList<>(); - List cacheKeyParameterPositions = new ArrayList<>(); + List interceptorBindings = new ArrayList<>(bindings.size() / 2); // initial capacity is a heuristic here... + List cacheKeyParameterPositions = new ArrayList<>(bindings.size() / 2); for (Annotation binding : bindings) { if (binding instanceof CacheKeyParameterPositions) { for (short position : ((CacheKeyParameterPositions) binding).value()) { diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionValidateAtStartTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionValidateAtStartTest.java new file mode 100644 index 0000000000000..9e685253a9bc4 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionValidateAtStartTest.java @@ -0,0 +1,23 @@ +package io.quarkus.flyway.test; + +import org.flywaydb.core.api.exception.FlywayValidateException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionValidateAtStartTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("validate-at-start-config.properties", "application.properties")) + .setExpectedException(FlywayValidateException.class); + + @Test + public void shouldNeverBeCalled() { + + } + +} diff --git a/extensions/flyway/deployment/src/test/resources/validate-at-start-config.properties b/extensions/flyway/deployment/src/test/resources/validate-at-start-config.properties new file mode 100644 index 0000000000000..a0810ce17fb35 --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/validate-at-start-config.properties @@ -0,0 +1,7 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test-quarkus-validate-at-start;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.validate-at-start=true diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainer.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainer.java index a42d295ad4771..0a956422d1a6b 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainer.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainer.java @@ -8,17 +8,21 @@ public class FlywayContainer { private final boolean cleanAtStart; private final boolean migrateAtStart; private final boolean repairAtStart; + + private final boolean validateAtStart; private final String dataSourceName; private final boolean hasMigrations; private final boolean createPossible; private final String id; public FlywayContainer(Flyway flyway, boolean cleanAtStart, boolean migrateAtStart, boolean repairAtStart, + boolean validateAtStart, String dataSourceName, boolean hasMigrations, boolean createPossible) { this.flyway = flyway; this.cleanAtStart = cleanAtStart; this.migrateAtStart = migrateAtStart; this.repairAtStart = repairAtStart; + this.validateAtStart = validateAtStart; this.dataSourceName = dataSourceName; this.hasMigrations = hasMigrations; this.createPossible = createPossible; @@ -41,6 +45,10 @@ public boolean isRepairAtStart() { return repairAtStart; } + public boolean isValidateAtStart() { + return validateAtStart; + } + public String getDataSourceName() { return dataSourceName; } diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainerProducer.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainerProducer.java index d7f4c727e3a54..71e3a19cb2501 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainerProducer.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainerProducer.java @@ -36,6 +36,7 @@ public FlywayContainer createFlyway(DataSource dataSource, String dataSourceName final Flyway flyway = new FlywayCreator(matchingRuntimeConfig, matchingBuildTimeConfig).withCallbacks(callbacks) .createFlyway(dataSource); return new FlywayContainer(flyway, matchingRuntimeConfig.cleanAtStart, matchingRuntimeConfig.migrateAtStart, - matchingRuntimeConfig.repairAtStart, dataSourceName, hasMigrations, createPossible); + matchingRuntimeConfig.repairAtStart, matchingRuntimeConfig.validateAtStart, dataSourceName, hasMigrations, + createPossible); } } diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java index 6162ab83fc048..9f9b05c9ada41 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java @@ -112,6 +112,13 @@ public static FlywayDataSourceRuntimeConfig defaultConfig() { @ConfigItem public boolean repairAtStart; + /** + * true to execute a Flyway validate command when the application starts, false otherwise. + * + */ + @ConfigItem + public boolean validateAtStart; + /** * Enable the creation of the history table if it does not exist already. */ diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java index cbfa3fb789e48..2bcaf79d62f87 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java @@ -72,6 +72,9 @@ public void doStartActions() { if (flywayContainer.isCleanAtStart()) { flywayContainer.getFlyway().clean(); } + if (flywayContainer.isValidateAtStart()) { + flywayContainer.getFlyway().validate(); + } if (flywayContainer.isRepairAtStart()) { flywayContainer.getFlyway().repair(); } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ChangeIngressRuleDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ChangeIngressRuleDecorator.java new file mode 100644 index 0000000000000..860a4f6c8586f --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ChangeIngressRuleDecorator.java @@ -0,0 +1,180 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.Optional; + +import io.dekorate.kubernetes.config.IngressRule; +import io.dekorate.kubernetes.config.Port; +import io.dekorate.kubernetes.decorator.AddIngressRuleDecorator; +import io.dekorate.kubernetes.decorator.Decorator; +import io.dekorate.kubernetes.decorator.NamedResourceDecorator; +import io.dekorate.utils.Strings; +import io.fabric8.kubernetes.api.builder.TypedVisitor; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressPathBuilder; +import io.fabric8.kubernetes.api.model.networking.v1.IngressRuleBuilder; +import io.fabric8.kubernetes.api.model.networking.v1.IngressServiceBackendBuilder; +import io.fabric8.kubernetes.api.model.networking.v1.IngressSpecBuilder; +import io.fabric8.kubernetes.api.model.networking.v1.ServiceBackendPort; +import io.fabric8.kubernetes.api.model.networking.v1.ServiceBackendPortBuilder; + +/** + * TODO: Workaround for https://github.com/quarkusio/quarkus/issues/28812 + * We need to remove the duplicate paths of the generated Ingress. The following logic can be removed after + * bumping the next Dekorate version that includes the fix: https://github.com/dekorateio/dekorate/pull/1092. + */ +public class ChangeIngressRuleDecorator extends NamedResourceDecorator { + + private static final String DEFAULT_PREFIX = "Prefix"; + + private final Optional defaultHostPort; + private final IngressRule rule; + + public ChangeIngressRuleDecorator(String name, Optional defaultHostPort, IngressRule rule) { + super(name); + this.defaultHostPort = defaultHostPort; + this.rule = rule; + } + + @Override + public void andThenVisit(IngressSpecBuilder spec, ObjectMeta meta) { + if (!spec.hasMatchingRule(existingRule -> Strings.equals(rule.getHost(), existingRule.getHost()))) { + spec.addNewRule() + .withHost(rule.getHost()) + .withNewHttp() + .addNewPath() + .withPathType(pathType()) + .withPath(path()) + .withNewBackend() + .withNewService() + .withName(serviceName()) + .withPort(createPort(defaultHostPort)) + .endService() + .endBackend() + .endPath() + .endHttp() + .endRule(); + } else { + spec.accept(new HostVisitor(defaultHostPort)); + } + } + + @Override + public Class[] after() { + return new Class[] { AddIngressRuleDecorator.class, RemoveDuplicateIngressRuleDecorator.class }; + } + + private String serviceName() { + return Strings.defaultIfEmpty(rule.getServiceName(), name); + } + + private String path() { + return Strings.defaultIfEmpty(rule.getPath(), defaultHostPort.map(p -> p.getPath()).orElse("/")); + } + + private String pathType() { + return Strings.defaultIfEmpty(rule.getPathType(), DEFAULT_PREFIX); + } + + private ServiceBackendPort createPort(Optional defaultHostPort) { + ServiceBackendPortBuilder builder = new ServiceBackendPortBuilder(); + if (Strings.isNotNullOrEmpty(rule.getServicePortName())) { + builder.withName(rule.getServicePortName()); + } else if (rule.getServicePortNumber() != null && rule.getServicePortNumber() >= 0) { + builder.withNumber(rule.getServicePortNumber()); + } else if (Strings.isNullOrEmpty(rule.getServiceName()) || Strings.equals(rule.getServiceName(), name)) { + // Trying to get the port from the service + Port servicePort = defaultHostPort + .orElseThrow(() -> new RuntimeException( + "Could not find any matching port to configure the Ingress Rule. Specify the " + + "service port using `kubernetes.ingress.service-port-name`")); + builder.withName(servicePort.getName()); + } else { + throw new RuntimeException("The service port for '" + rule.getServiceName() + "' was not set. Specify one " + + "using `kubernetes.ingress.service-port-name`"); + } + + return builder.build(); + } + + private class HostVisitor extends TypedVisitor { + + private final Optional defaultHostPort; + + public HostVisitor(Optional defaultHostPort) { + this.defaultHostPort = defaultHostPort; + } + + @Override + public void visit(IngressRuleBuilder existingRule) { + if (Strings.equals(existingRule.getHost(), rule.getHost())) { + if (!existingRule.hasHttp()) { + existingRule.withNewHttp() + .addNewPath() + .withPathType(pathType()) + .withPath(path()) + .withNewBackend() + .withNewService() + .withName(serviceName()) + .withPort(createPort(defaultHostPort)) + .endService() + .endBackend() + .endPath().endHttp(); + } else if (existingRule.getHttp().getPaths().stream() + .noneMatch(p -> Strings.equals(p.getPath(), path()) && Strings.equals(p.getPathType(), pathType()))) { + existingRule.editHttp() + .addNewPath() + .withPathType(pathType()) + .withPath(path()) + .withNewBackend() + .withNewService() + .withName(serviceName()) + .withPort(createPort(defaultHostPort)) + .endService() + .endBackend() + .endPath().endHttp(); + } else { + existingRule.accept(new PathVisitor(defaultHostPort)); + } + } + } + } + + private class PathVisitor extends TypedVisitor { + + private final Optional defaultHostPort; + + public PathVisitor(Optional defaultHostPort) { + this.defaultHostPort = defaultHostPort; + } + + @Override + public void visit(HTTPIngressPathBuilder existingPath) { + if (Strings.equals(existingPath.getPath(), rule.getPath())) { + if (!existingPath.hasBackend()) { + existingPath.withNewBackend() + .withNewService() + .withName(serviceName()) + .withPort(createPort(defaultHostPort)) + .endService() + .endBackend(); + } else { + existingPath.accept(new ServiceVisitor(defaultHostPort)); + } + } + } + } + + private class ServiceVisitor extends TypedVisitor { + + private final Optional defaultHostPort; + + public ServiceVisitor(Optional defaultHostPort) { + this.defaultHostPort = defaultHostPort; + } + + @Override + public void visit(IngressServiceBackendBuilder service) { + service.withName(Strings.defaultIfEmpty(rule.getServiceName(), name)).withPort(createPort(defaultHostPort)); + } + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/IngressConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/IngressConfig.java index cfc18f2d69278..71072ff1cc935 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/IngressConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/IngressConfig.java @@ -33,4 +33,10 @@ public class IngressConfig { @ConfigItem Map tls; + /** + * Custom rules for the current ingress resource. + */ + @ConfigItem + Map rules; + } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/IngressRuleConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/IngressRuleConfig.java new file mode 100644 index 0000000000000..d31723b586f24 --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/IngressRuleConfig.java @@ -0,0 +1,48 @@ +package io.quarkus.kubernetes.deployment; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class IngressRuleConfig { + + /** + * The host under which the rule is going to be used. + */ + @ConfigItem + String host; + + /** + * The path under which the rule is going to be used. Default is "/". + */ + @ConfigItem(defaultValue = "/") + String path; + + /** + * The path type strategy to use by the Ingress rule. Default is "Prefix". + */ + @ConfigItem(defaultValue = "Prefix") + String pathType; + + /** + * The service name to be used by this Ingress rule. Default is the generated service name of the application. + */ + @ConfigItem + Optional serviceName; + + /** + * The service port name to be used by this Ingress rule. Default is the port name of the generated service of + * the application. + */ + @ConfigItem + Optional servicePortName; + + /** + * The service port number to be used by this Ingress rule. This is only used when the servicePortName is not set. + */ + @ConfigItem + Optional servicePortNumber; + +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RemoveDuplicateIngressRuleDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RemoveDuplicateIngressRuleDecorator.java new file mode 100644 index 0000000000000..90a4c2c912b6e --- /dev/null +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/RemoveDuplicateIngressRuleDecorator.java @@ -0,0 +1,37 @@ +package io.quarkus.kubernetes.deployment; + +import io.dekorate.kubernetes.decorator.AddIngressRuleDecorator; +import io.dekorate.kubernetes.decorator.Decorator; +import io.dekorate.kubernetes.decorator.NamedResourceDecorator; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.networking.v1.IngressSpecBuilder; + +/** + * TODO: Workaround for https://github.com/quarkusio/quarkus/issues/28812 + * We need to remove the duplicate paths of the generated Ingress. The following logic can be removed after + * bumping the next Dekorate version that includes the fix: https://github.com/dekorateio/dekorate/pull/1092. + */ +public class RemoveDuplicateIngressRuleDecorator extends NamedResourceDecorator { + + public RemoveDuplicateIngressRuleDecorator(String name) { + super(name); + } + + @Override + public void andThenVisit(IngressSpecBuilder spec, ObjectMeta meta) { + if (spec.hasRules()) { + spec.editMatchingRule(rule -> { + rule.editHttp() + .removeMatchingFromPaths(path -> rule.getHttp().getPaths().stream() + .filter(p -> p.hashCode() == path.hashCode()).count() > 1) + .endHttp(); + return true; + }); + } + } + + @Override + public Class[] after() { + return new Class[] { AddIngressRuleDecorator.class }; + } +} diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java index a62abce41fbfb..30da93f016091 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/VanillaKubernetesProcessor.java @@ -1,6 +1,7 @@ package io.quarkus.kubernetes.deployment; +import static io.dekorate.kubernetes.decorator.AddServiceResourceDecorator.distinct; import static io.quarkus.kubernetes.deployment.Constants.DEFAULT_HTTP_PORT; import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT_GROUP; import static io.quarkus.kubernetes.deployment.Constants.DEPLOYMENT_VERSION; @@ -19,6 +20,8 @@ import io.dekorate.kubernetes.annotation.ServiceType; import io.dekorate.kubernetes.config.EnvBuilder; import io.dekorate.kubernetes.config.IngressBuilder; +import io.dekorate.kubernetes.config.IngressRuleBuilder; +import io.dekorate.kubernetes.config.Port; import io.dekorate.kubernetes.decorator.AddAnnotationDecorator; import io.dekorate.kubernetes.decorator.AddEnvVarDecorator; import io.dekorate.kubernetes.decorator.AddIngressTlsDecorator; @@ -173,6 +176,25 @@ public List createDecorators(ApplicationInfoBuildItem applic result.add(new DecoratorBuildItem(KUBERNETES, new AddAnnotationDecorator(name, annotation.getKey(), annotation.getValue(), INGRESS))); } + // TODO: Workaround for https://github.com/quarkusio/quarkus/issues/28812 + // We need to remove the duplicate paths of the generated Ingress. The following logic can be removed after + // bumping the next Dekorate version that includes the fix: https://github.com/dekorateio/dekorate/pull/1092. + result.add(new DecoratorBuildItem(KUBERNETES, new RemoveDuplicateIngressRuleDecorator(name))); + Optional defaultHostPort = KubernetesCommonHelper.combinePorts(ports, config).values().stream() + .filter(distinct(p -> p.getName())) + .findFirst(); + + for (IngressRuleConfig rule : config.ingress.rules.values()) { + result.add(new DecoratorBuildItem(KUBERNETES, new ChangeIngressRuleDecorator(name, defaultHostPort, + new IngressRuleBuilder() + .withHost(rule.host) + .withPath(rule.path) + .withPathType(rule.pathType) + .withServiceName(rule.serviceName.orElse(null)) + .withServicePortName(rule.servicePortName.orElse(null)) + .withServicePortNumber(rule.servicePortNumber.orElse(-1)) + .build()))); + } } if (config.getReplicas() != 1) { diff --git a/extensions/panache/hibernate-orm-panache-common/deployment/src/main/java/io/quarkus/hibernate/orm/panache/common/deployment/PanacheJpaCommonResourceProcessor.java b/extensions/panache/hibernate-orm-panache-common/deployment/src/main/java/io/quarkus/hibernate/orm/panache/common/deployment/PanacheJpaCommonResourceProcessor.java index d39b568b528b2..e32cefc3f6ea9 100644 --- a/extensions/panache/hibernate-orm-panache-common/deployment/src/main/java/io/quarkus/hibernate/orm/panache/common/deployment/PanacheJpaCommonResourceProcessor.java +++ b/extensions/panache/hibernate-orm-panache-common/deployment/src/main/java/io/quarkus/hibernate/orm/panache/common/deployment/PanacheJpaCommonResourceProcessor.java @@ -17,13 +17,16 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.util.JandexUtil; +import io.quarkus.hibernate.orm.deployment.HibernateOrmEnabled; import io.quarkus.hibernate.orm.deployment.JpaModelBuildItem; import io.quarkus.hibernate.orm.panache.common.runtime.PanacheHibernateRecorder; +@BuildSteps(onlyIf = HibernateOrmEnabled.class) public final class PanacheJpaCommonResourceProcessor { private static final DotName DOTNAME_NAMED_QUERY = DotName.createSimple(NamedQuery.class.getName()); diff --git a/extensions/panache/hibernate-orm-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/orm/panache/kotlin/deployment/test/MyEntity.kt b/extensions/panache/hibernate-orm-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/orm/panache/kotlin/deployment/test/MyEntity.kt new file mode 100644 index 0000000000000..14a5e306ae467 --- /dev/null +++ b/extensions/panache/hibernate-orm-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/orm/panache/kotlin/deployment/test/MyEntity.kt @@ -0,0 +1,13 @@ +package io.quarkus.hibernate.orm.panache.kotlin.deployment.test + +import io.quarkus.hibernate.orm.panache.kotlin.PanacheCompanion +import io.quarkus.hibernate.orm.panache.kotlin.PanacheEntity +import javax.persistence.Entity + +@Entity +class MyEntity : PanacheEntity() { + companion object: PanacheCompanion { + } + + lateinit var name: String +} diff --git a/extensions/panache/hibernate-orm-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/orm/panache/kotlin/deployment/test/config/ConfigEnabledFalseTest.kt b/extensions/panache/hibernate-orm-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/orm/panache/kotlin/deployment/test/config/ConfigEnabledFalseTest.kt new file mode 100644 index 0000000000000..b292b767d9b9a --- /dev/null +++ b/extensions/panache/hibernate-orm-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/orm/panache/kotlin/deployment/test/config/ConfigEnabledFalseTest.kt @@ -0,0 +1,29 @@ +package io.quarkus.hibernate.orm.panache.kotlin.deployment.test.config + +import io.quarkus.arc.Arc +import io.quarkus.hibernate.orm.panache.kotlin.deployment.test.MyEntity +import io.quarkus.test.QuarkusUnitTest +import org.jboss.shrinkwrap.api.spec.JavaArchive +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import javax.persistence.EntityManagerFactory + +class ConfigEnabledFalseTest { + companion object { + @RegisterExtension + val config = QuarkusUnitTest() + .withApplicationRoot { jar: JavaArchive -> jar.addClass(MyEntity::class.java) } + .withConfigurationResource("application-test.properties") + // We shouldn't get any build error caused by Panache consuming build items that are not produced + // See https://github.com/quarkusio/quarkus/issues/28842 + .overrideConfigKey("quarkus.hibernate-orm.enabled", "false") + } + + @Test + fun startsWithoutError() { + // Quarkus started without problem, even though the Panache extension is present. + // Just check that Hibernate ORM is disabled. + Assertions.assertNull(Arc.container().instance(EntityManagerFactory::class.java).get()) + } +} \ No newline at end of file diff --git a/extensions/panache/hibernate-orm-panache/deployment/pom.xml b/extensions/panache/hibernate-orm-panache/deployment/pom.xml index 6175a737da04c..944df5b5c88b0 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/pom.xml +++ b/extensions/panache/hibernate-orm-panache/deployment/pom.xml @@ -68,6 +68,11 @@ quarkus-resteasy-deployment test + + org.assertj + assertj-core + test + diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/config/ConfigEnabledFalseTest.java b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/config/ConfigEnabledFalseTest.java new file mode 100644 index 0000000000000..300740555e537 --- /dev/null +++ b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/config/ConfigEnabledFalseTest.java @@ -0,0 +1,31 @@ +package io.quarkus.hibernate.orm.panache.deployment.test.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import javax.persistence.EntityManagerFactory; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.hibernate.orm.panache.deployment.test.MyEntity; +import io.quarkus.test.QuarkusUnitTest; + +public class ConfigEnabledFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClass(MyEntity.class)) + .withConfigurationResource("application-test.properties") + // We shouldn't get any build error caused by Panache consuming build items that are not produced + // See https://github.com/quarkusio/quarkus/issues/28842 + .overrideConfigKey("quarkus.hibernate-orm.enabled", "false"); + + @Test + public void startsWithoutError() { + // Quarkus started without problem, even though the Panache extension is present. + // Just check that Hibernate ORM is disabled. + assertThat(Arc.container().instance(EntityManagerFactory.class).get()) + .isNull(); + } +} diff --git a/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/deployment/PanacheJpaCommonResourceProcessor.java b/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/deployment/PanacheJpaCommonResourceProcessor.java index ff3b137fbf9c7..dd5719239404b 100644 --- a/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/deployment/PanacheJpaCommonResourceProcessor.java +++ b/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/deployment/PanacheJpaCommonResourceProcessor.java @@ -23,16 +23,19 @@ import io.quarkus.deployment.IsTest; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.util.JandexUtil; import io.quarkus.gizmo.ClassCreator; +import io.quarkus.hibernate.orm.deployment.HibernateOrmEnabled; import io.quarkus.hibernate.orm.deployment.JpaModelBuildItem; import io.quarkus.hibernate.reactive.panache.common.runtime.PanacheHibernateRecorder; import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactionalInterceptor; import io.quarkus.hibernate.reactive.panache.common.runtime.TestReactiveTransactionalInterceptor; +@BuildSteps(onlyIf = HibernateOrmEnabled.class) public final class PanacheJpaCommonResourceProcessor { private static final DotName DOTNAME_NAMED_QUERY = DotName.createSimple(NamedQuery.class.getName()); diff --git a/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/reactive/panache/kotlin/deployment/test/MyEntity.kt b/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/reactive/panache/kotlin/deployment/test/MyEntity.kt new file mode 100644 index 0000000000000..07f3c1ee26b73 --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/reactive/panache/kotlin/deployment/test/MyEntity.kt @@ -0,0 +1,13 @@ +package io.quarkus.hibernate.reactive.panache.kotlin.deployment.test + +import io.quarkus.hibernate.reactive.panache.kotlin.PanacheCompanion +import io.quarkus.hibernate.reactive.panache.kotlin.PanacheEntity +import javax.persistence.Entity + +@Entity +class MyEntity : PanacheEntity() { + companion object: PanacheCompanion { + } + + lateinit var name: String +} diff --git a/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/reactive/panache/kotlin/deployment/test/config/ConfigEnabledFalseTest.kt b/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/reactive/panache/kotlin/deployment/test/config/ConfigEnabledFalseTest.kt new file mode 100644 index 0000000000000..bf749e2304a50 --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/test/kotlin/io/quarkus/hibernate/reactive/panache/kotlin/deployment/test/config/ConfigEnabledFalseTest.kt @@ -0,0 +1,30 @@ +package io.quarkus.hibernate.reactive.panache.kotlin.deployment.test.config + +import io.quarkus.arc.Arc +import io.quarkus.hibernate.reactive.panache.kotlin.deployment.test.MyEntity +import io.quarkus.test.QuarkusUnitTest +import org.hibernate.reactive.mutiny.Mutiny +import org.hibernate.reactive.mutiny.impl.MutinySessionFactoryImpl +import org.jboss.shrinkwrap.api.spec.JavaArchive +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class ConfigEnabledFalseTest { + companion object { + @RegisterExtension + val config = QuarkusUnitTest() + .withApplicationRoot { jar: JavaArchive -> jar.addClass(MyEntity::class.java) } + .withConfigurationResource("application.properties") + // We shouldn't get any build error caused by Panache consuming build items that are not produced + // See https://github.com/quarkusio/quarkus/issues/28842 + .overrideConfigKey("quarkus.hibernate-orm.enabled", "false") + } + + @Test + fun startsWithoutError() { + // Quarkus started without problem, even though the Panache extension is present. + // Just check that Hibernate Reactive is disabled. + Assertions.assertNull(Arc.container().instance(Mutiny.SessionFactory::class.java).get()) + } +} \ No newline at end of file diff --git a/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/test/resources/application.properties b/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/test/resources/application.properties new file mode 100644 index 0000000000000..707f58e5af781 --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-kotlin/deployment/src/test/resources/application.properties @@ -0,0 +1,7 @@ +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=hibernate_orm_test +quarkus.datasource.password=hibernate_orm_test +quarkus.datasource.reactive.url=${postgres.reactive.url} +quarkus.datasource.devservices.enabled=false + +quarkus.hibernate-orm.database.generation=drop-and-create diff --git a/extensions/panache/hibernate-reactive-panache/deployment/pom.xml b/extensions/panache/hibernate-reactive-panache/deployment/pom.xml index 443ba373006e1..2f2aafb53fb4f 100644 --- a/extensions/panache/hibernate-reactive-panache/deployment/pom.xml +++ b/extensions/panache/hibernate-reactive-panache/deployment/pom.xml @@ -55,6 +55,11 @@ rest-assured test + + org.assertj + assertj-core + test + diff --git a/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/config/ConfigEnabledFalseTest.java b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/config/ConfigEnabledFalseTest.java new file mode 100644 index 0000000000000..461c1014552e4 --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/config/ConfigEnabledFalseTest.java @@ -0,0 +1,30 @@ +package io.quarkus.hibernate.reactive.panache.test.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hibernate.reactive.mutiny.Mutiny; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.hibernate.reactive.panache.test.MyEntity; +import io.quarkus.test.QuarkusUnitTest; + +public class ConfigEnabledFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClass(MyEntity.class)) + .withConfigurationResource("application.properties") + // We shouldn't get any build error caused by Panache consuming build items that are not produced + // See https://github.com/quarkusio/quarkus/issues/28842 + .overrideConfigKey("quarkus.hibernate-orm.enabled", "false"); + + @Test + public void startsWithoutError() { + // Quarkus started without problem, even though the Panache extension is present. + // Just check that Hibernate ORM is disabled. + assertThat(Arc.container().instance(Mutiny.SessionFactory.class).get()) + .isNull(); + } +} diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 8369b4ac0cfa5..5c82459d08232 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -159,7 +159,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv } Map localeToInterface = new HashMap<>(); for (ClassInfo localizedInterface : localized) { - String locale = localizedInterface.classAnnotation(Names.LOCALIZED).value().asString(); + String locale = localizedInterface.declaredAnnotation(Names.LOCALIZED).value().asString(); if (defaultLocale.equals(locale)) { throw new MessageBundleException( String.format( @@ -234,7 +234,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv beanRegistration.getContext().configure(bundleInterface.name()).addType(bundle.getDefaultBundleInterface().name()) // The default message bundle - add both @Default and @Localized .addQualifier(DotNames.DEFAULT).addQualifier().annotation(Names.LOCALIZED) - .addValue("value", getDefaultLocale(bundleInterface.classAnnotation(Names.BUNDLE), locales)).done() + .addValue("value", getDefaultLocale(bundleInterface.declaredAnnotation(Names.BUNDLE), locales)).done() .unremovable() .scope(Singleton.class).creator(mc -> { // Just create a new instance of the generated class @@ -247,7 +247,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv for (ClassInfo localizedInterface : bundle.getLocalizedInterfaces().values()) { beanRegistration.getContext().configure(localizedInterface.name()) .addType(bundle.getDefaultBundleInterface().name()) - .addQualifier(localizedInterface.classAnnotation(Names.LOCALIZED)) + .addQualifier(localizedInterface.declaredAnnotation(Names.LOCALIZED)) .unremovable() .scope(Singleton.class).creator(mc -> { // Just create a new instance of the generated class @@ -770,8 +770,8 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d ClassInfo bundleInterface = bundleInterfaceWrapper.getClassInfo(); LOGGER.debugf("Generate bundle implementation for %s", bundleInterface); AnnotationInstance bundleAnnotation = defaultBundleInterface != null - ? defaultBundleInterface.classAnnotation(Names.BUNDLE) - : bundleInterface.classAnnotation(Names.BUNDLE); + ? defaultBundleInterface.declaredAnnotation(Names.BUNDLE) + : bundleInterface.declaredAnnotation(Names.BUNDLE); AnnotationValue nameValue = bundleAnnotation.value(); String bundleName = nameValue != null ? nameValue.asString() : MessageBundle.DEFAULT_NAME; AnnotationValue defaultKeyValue = bundleAnnotation.value(BUNDLE_DEFAULT_KEY); @@ -864,7 +864,7 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d if (messageTemplate.contains("}")) { if (defaultBundleInterface != null) { if (locale == null) { - AnnotationInstance localizedAnnotation = bundleInterface.classAnnotation(Names.LOCALIZED); + AnnotationInstance localizedAnnotation = bundleInterface.declaredAnnotation(Names.LOCALIZED); locale = localizedAnnotation.value().asString(); } templateId = bundleName + "_" + locale + "_" + key; diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java index ec63c94dca3d6..400fe6ba49a35 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java @@ -44,7 +44,6 @@ final class Names { static final DotName LOCATES = DotName.createSimple(Locates.class.getName()); static final DotName CHECKED_TEMPLATE = DotName.createSimple(io.quarkus.qute.CheckedTemplate.class.getName()); static final DotName TEMPLATE_ENUM = DotName.createSimple(TemplateEnum.class.getName()); - static final DotName CHECKED_FRAGMENT = DotName.createSimple(io.quarkus.qute.CheckedFragment.class.getName()); private Names() { } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 973576be7d134..d9317ba57960e 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -151,6 +151,7 @@ public class QuteProcessor { private static final String CHECKED_TEMPLATE_REQUIRE_TYPE_SAFE = "requireTypeSafeExpressions"; private static final String CHECKED_TEMPLATE_BASE_PATH = "basePath"; private static final String CHECKED_TEMPLATE_DEFAULT_NAME = "defaultName"; + private static final String IGNORE_FRAGMENTS = "ignoreFragments"; private static final String BASE_PATH = "templates"; private static final Set ITERATION_METADATA_KEYS = Set.of("count", "index", "indexParity", "hasNext", "odd", @@ -312,11 +313,7 @@ List collectCheckedTemplates(BeanArchiveIndexBuildItem throw new TemplateException("Incompatible checked template return type: " + methodInfo.returnType() + " only " + supportedAdaptors); } - String fragmentId = null; - if (methodInfo.hasDeclaredAnnotation(Names.CHECKED_FRAGMENT)) { - fragmentId = getCheckedFragmentId(methodInfo, annotation); - } - + String fragmentId = getCheckedFragmentId(methodInfo, annotation); StringBuilder templatePathBuilder = new StringBuilder(); AnnotationValue basePathValue = annotation.value(CHECKED_TEMPLATE_BASE_PATH); if (basePathValue != null && !basePathValue.asString().equals(CheckedTemplate.DEFAULTED)) { @@ -344,7 +341,8 @@ && isNotLocatedByCustomTemplateLocator(locatorPatternsBuildItem.getLocationPatte templatePath)) { List startsWith = new ArrayList<>(); for (String filePath : filePaths.getFilePaths()) { - if (filePath.startsWith(templatePath)) { + if (filePath.startsWith(templatePath) + && filePath.charAt(templatePath.length()) == '.') { startsWith.add(filePath); } } @@ -402,6 +400,16 @@ private String getCheckedTemplateName(MethodInfo method, AnnotationInstance chec } private String getCheckedFragmentId(MethodInfo method, AnnotationInstance checkedTemplateAnnotation) { + AnnotationValue ignoreFragmentsValue = checkedTemplateAnnotation.value(IGNORE_FRAGMENTS); + if (ignoreFragmentsValue != null && ignoreFragmentsValue.asBoolean()) { + return null; + } + String methodName = method.name(); + // the id is the part after the last occurence of a dollar sign + int idx = methodName.lastIndexOf('$'); + if (idx == -1 || idx == methodName.length()) { + return null; + } AnnotationValue nameValue = checkedTemplateAnnotation.value(CHECKED_TEMPLATE_DEFAULT_NAME); String defaultName; if (nameValue == null) { @@ -409,14 +417,6 @@ private String getCheckedFragmentId(MethodInfo method, AnnotationInstance checke } else { defaultName = nameValue.asString(); } - String methodName = method.name(); - // the id is the part after the last occurence of a dollar sign - int idx = methodName.lastIndexOf('$'); - if (idx == -1 || idx == methodName.length()) { - throw new TemplateException( - "[" + method.name() + "] is not a valid name of a checked fragment method: " - + method.declaringClass().name().withoutPackagePrefix() + "." + method.name() + "()"); - } return defaultedName(defaultName, methodName.substring(idx + 1, methodName.length())); } @@ -1998,6 +1998,17 @@ private static Type resolveType(AnnotationTarget member, Match match, IndexView } // If needed attempt to resolve the type variables using the declaring type if (Types.containsTypeVariable(matchType)) { + + if (match.clazz == null) { + if (member.kind() == Kind.METHOD && match.isPrimitive()) { + final Type wrapperType = Types.box(match.type.asPrimitiveType()); + match.setValues(index.getClassByName(wrapperType.name()), wrapperType); + } else { + // we can't resolve type without class + return matchType; + } + } + // First get the type closure of the current match type Set closure = Types.getTypeClosure(match.clazz, Types.buildResolvedMap( @@ -2046,11 +2057,18 @@ private static Type resolveType(AnnotationTarget member, Match match, IndexView for (int i = 1; i < params.size() && (i - 1) < paramExpressions.size(); i++) { // whether params.get(i) has same type as the extension base (e.g. T) if (params.get(i).name().equals(extensionMatchBase.name())) { - final var paramMatch = results.get(paramExpressions.get(i - 1).toOriginalString()); - // if all T params are not of exactly same type, we do not try to determine - // right superclass/interface as it's expensive - if (paramMatch != null && !match.type().equals(paramMatch.type())) { - return matchType; + var paramMatch = results.get(paramExpressions.get(i - 1).toOriginalString()); + if (paramMatch != null) { + Type paramMatchType = paramMatch.type(); + if (paramMatch.isPrimitive()) { + // use boxed type + paramMatchType = Types.box(paramMatch.type()); + } + // if all T params are not of exactly same type, we do not try to determine + // right superclass/interface as it's expensive + if (!match.type().equals(paramMatchType)) { + return matchType; + } } } } @@ -2744,7 +2762,7 @@ void collectTemplateDataAnnotations(BeanArchiveIndexBuildItem beanArchiveIndex, targetEnum); continue; } - if (targetEnum.classAnnotation(ValueResolverGenerator.TEMPLATE_DATA) != null) { + if (targetEnum.declaredAnnotation(ValueResolverGenerator.TEMPLATE_DATA) != null) { LOGGER.debugf("@TemplateEnum declared on %s is ignored: enum is annotated with @TemplateData", targetEnum); continue; } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/Item.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/Item.java index 82a0900080fe8..1e54e24aceed2 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/Item.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/Item.java @@ -19,4 +19,8 @@ public OtherItem[] getOtherItems() { return otherItems; } + public int getPrimitiveId() { + return 9; + } + } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ItemWithName.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ItemWithName.java index 73b8204c960e8..06284db56da43 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ItemWithName.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/ItemWithName.java @@ -11,6 +11,10 @@ public Integer getId() { return 2; } + public int getPrimitiveId() { + return getId() * -1; + } + public Name getName() { return name; } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OrOperatorTemplateExtensionFailureTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OrOperatorTemplateExtensionFailureTest.java index d8f0885a4a699..229a863fbd64e 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OrOperatorTemplateExtensionFailureTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OrOperatorTemplateExtensionFailureTest.java @@ -23,6 +23,7 @@ public class OrOperatorTemplateExtensionFailureTest { "{@io.quarkus.qute.deployment.typesafe.Item item}\n" + " {data:item.name.or(item2.name).pleaseMakeMyCaseUpper}\n" + " {item.name.or(item2.name).pleaseMakeMyCaseUpper}\n" + + " {item.getPrimitiveId().or(item2.getPrimitiveId()).missingMethod()}\n" + "{/for}\n"), "templates/item.html")) .assertException(t -> { @@ -43,6 +44,9 @@ public class OrOperatorTemplateExtensionFailureTest { assertTrue(te.getMessage().contains( "{data:item.name.or(item2.name).pleaseMakeMyCaseUpper}: Property/method [pleaseMakeMyCaseUpper] not found on class [java.lang.String]"), te.getMessage()); + assertTrue(te.getMessage().contains( + "{item.getPrimitiveId().or(item2.getPrimitiveId()).missingMethod()}: Property/method [missingMethod()]"), + te.getMessage()); }); @Test diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OrOperatorTemplateExtensionTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OrOperatorTemplateExtensionTest.java index f2219c17a9e5d..76280ac44ff6d 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OrOperatorTemplateExtensionTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OrOperatorTemplateExtensionTest.java @@ -16,6 +16,8 @@ public class OrOperatorTemplateExtensionTest { public static final String ITEM_NAME = "Test Name"; public static final String ITEM_WITH_NAME = "itemWithName"; public static final String ITEM = "item"; + public static final String ITEMS = "items"; + public static final String ITEMS_2 = "items2"; @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() @@ -24,10 +26,14 @@ public class OrOperatorTemplateExtensionTest { .addAsResource(new StringAsset( "{@io.quarkus.qute.deployment.typesafe.ItemWithName itemWithName}\n" + "{@io.quarkus.qute.deployment.typesafe.Item item}\n" + + "{@io.quarkus.qute.deployment.typesafe.Item[] items}\n" + + "{@io.quarkus.qute.deployment.typesafe.Item[] items2}\n" + "{#for otherItem in item.otherItems}\n" + - "{missing.or(alsoMissing.or('item id is: ')).toLowerCase}" + + "{missing.or(alsoMissing.or('result is: ')).toLowerCase}" + "{item.name.or(itemWithName.name).toUpperCase}" + - "{otherItem.id.or(itemWithName.id).longValue()}" + + "{items.or(items2).length}" + // test arrays + "{otherItem.id.or(itemWithName.id).longValue()}" + // tests boxed type + "{otherItem.getPrimitiveId().or(itemWithName.getPrimitiveId()).longValue()}" + // tests primitive type "{/for}\n"), "templates/item.html")); @@ -36,18 +42,22 @@ public class OrOperatorTemplateExtensionTest { @Test public void test() { - final String expected = "item id is: " + ITEM_NAME.toUpperCase(); + final String expected = "result is: " + ITEM_NAME.toUpperCase(); final ItemWithName itemWithName = new ItemWithName(new ItemWithName.Name()); - // id comes from OtherItem, name is String and toUpperCase is method from String - assertEquals(expected + OtherItem.ID, - item.data(ITEM, new Item(ITEM_NAME.toUpperCase(), new OtherItem()), ITEM_WITH_NAME, itemWithName).render() + // ids comes from OtherItem, name is String and toUpperCase is method from String + Item[] items = new Item[4]; + assertEquals(expected + items.length + OtherItem.ID + OtherItem.PRIMITIVE_ID, + item.data(ITEM, new Item(ITEM_NAME.toUpperCase(), new OtherItem()), ITEM_WITH_NAME, itemWithName, + ITEMS, items, ITEMS_2, null).render() .trim()); - // id comes from ItemWithName, name comes from ItemWithName.Name and toUpperCase is regular method + // ids comes from ItemWithName, name comes from ItemWithName.Name and toUpperCase is regular method + Item[] items2 = new Item[2]; assertEquals( - expected + itemWithName.getId(), - item.data(ITEM, new Item(null, (OtherItem) null), ITEM_WITH_NAME, itemWithName).render().trim()); + expected + items2.length + itemWithName.getId() + itemWithName.getPrimitiveId(), + item.data(ITEM, new Item(null, (OtherItem) null), ITEM_WITH_NAME, itemWithName, + ITEMS, null, ITEMS_2, items2).render().trim()); } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OtherItem.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OtherItem.java index d3d6d0caa33e5..26cabb8f224f3 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OtherItem.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/OtherItem.java @@ -3,9 +3,14 @@ public class OtherItem { static final int ID = 1; + static final int PRIMITIVE_ID = ID * -1; public Integer getId() { return ID; } + public int getPrimitiveId() { + return PRIMITIVE_ID; + } + } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateFragmentTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateFragmentTest.java index 1c418fcb80bbe..6f7f7ba7af2cb 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateFragmentTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateFragmentTest.java @@ -8,7 +8,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.qute.CheckedFragment; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import io.quarkus.test.QuarkusUnitTest; @@ -35,7 +34,6 @@ public static class Templates { static native TemplateInstance items(List items); - @CheckedFragment static native TemplateInstance items$item(Item it); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateIgnoreFragmentsTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateIgnoreFragmentsTest.java new file mode 100644 index 0000000000000..81714d85e55cc --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/CheckedTemplateIgnoreFragmentsTest.java @@ -0,0 +1,41 @@ +package io.quarkus.qute.deployment.typesafe.fragment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.test.QuarkusUnitTest; + +public class CheckedTemplateIgnoreFragmentsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Templates.class, Item.class) + .addAsResource(new StringAsset( + "{#each items}{it.name}{/each}"), + "templates/CheckedTemplateIgnoreFragmentsTest/items.html") + .addAsResource(new StringAsset("{it.name}"), + "templates/CheckedTemplateIgnoreFragmentsTest/items$item.html")); + + @Test + public void testFragment() { + assertEquals("Foo", Templates.items(List.of(new Item("Foo"))).render()); + assertEquals("Foo", Templates.items$item(new Item("Foo")).render()); + } + + @CheckedTemplate(ignoreFragments = true) + public static class Templates { + + static native TemplateInstance items(List items); + + static native TemplateInstance items$item(Item it); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/ComplexCheckedTemplateFragmentTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/ComplexCheckedTemplateFragmentTest.java index 5ce4250291ab7..07e63075eba1b 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/ComplexCheckedTemplateFragmentTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/ComplexCheckedTemplateFragmentTest.java @@ -8,7 +8,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.qute.CheckedFragment; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import io.quarkus.test.QuarkusUnitTest; @@ -42,10 +41,8 @@ public static class Templates { static native TemplateInstance items(List items); - @CheckedFragment static native TemplateInstance items$item_b(List foo, int size); - @CheckedFragment static native TemplateInstance items$item_a(List foo); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/InvalidMethodNameCheckedTemplateFragmentTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/InvalidMethodNameCheckedTemplateFragmentTest.java index 5266dfce2cb32..220dc85a51674 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/InvalidMethodNameCheckedTemplateFragmentTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/InvalidMethodNameCheckedTemplateFragmentTest.java @@ -10,7 +10,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.qute.CheckedFragment; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateException; import io.quarkus.qute.TemplateInstance; @@ -36,7 +35,7 @@ public class InvalidMethodNameCheckedTemplateFragmentTest { } assertNotNull(te, t.getMessage()); assertEquals( - "[item] is not a valid name of a checked fragment method: InvalidMethodNameCheckedTemplateFragmentTest$Templates.item()", + "No template matching the path InvalidMethodNameCheckedTemplateFragmentTest/item could be found for: io.quarkus.qute.deployment.typesafe.fragment.InvalidMethodNameCheckedTemplateFragmentTest$Templates.item", te.getMessage()); });; @@ -50,7 +49,6 @@ public static class Templates { static native TemplateInstance items(List items); - @CheckedFragment static native TemplateInstance item(Item it); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/InvalidParamTypeCheckedTemplateFragmentTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/InvalidParamTypeCheckedTemplateFragmentTest.java index 64d2da127d2be..881136fe9f410 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/InvalidParamTypeCheckedTemplateFragmentTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/InvalidParamTypeCheckedTemplateFragmentTest.java @@ -10,7 +10,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.qute.CheckedFragment; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateException; import io.quarkus.qute.TemplateInstance; @@ -50,7 +49,6 @@ public static class Templates { static native TemplateInstance items(List items); - @CheckedFragment static native TemplateInstance items$item(String it); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/MissingCheckedTemplateFragmentTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/MissingCheckedTemplateFragmentTest.java index dced8cbdceaad..ad9e732cd17b6 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/MissingCheckedTemplateFragmentTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/MissingCheckedTemplateFragmentTest.java @@ -10,7 +10,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.qute.CheckedFragment; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateException; import io.quarkus.qute.TemplateInstance; @@ -49,7 +48,6 @@ public static class Templates { static native TemplateInstance items(List items); - @CheckedFragment static native TemplateInstance items$item(Item it); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/MissingParamCheckedTemplateFragmentTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/MissingParamCheckedTemplateFragmentTest.java index 37597b1092a10..1d72574b497d7 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/MissingParamCheckedTemplateFragmentTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/typesafe/fragment/MissingParamCheckedTemplateFragmentTest.java @@ -10,7 +10,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.qute.CheckedFragment; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateException; import io.quarkus.qute.TemplateInstance; @@ -50,7 +49,6 @@ public static class Templates { static native TemplateInstance items(List items); - @CheckedFragment static native TemplateInstance items$item(); } diff --git a/extensions/redis-client/runtime/pom.xml b/extensions/redis-client/runtime/pom.xml index 93c70cbe866e9..623959b846e62 100644 --- a/extensions/redis-client/runtime/pom.xml +++ b/extensions/redis-client/runtime/pom.xml @@ -107,12 +107,18 @@ test-containers + + redis/redis-stack:7.0.2-RC2 + maven-surefire-plugin false + + ${redis.base.image} + @@ -124,5 +130,19 @@ + + + redis-5 + + redis:5 + + + + + redis-6 + + redis:6 + + diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractSortedSetCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractSortedSetCommands.java index a23664dec32c6..d0e63dee9960c 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractSortedSetCommands.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractSortedSetCommands.java @@ -28,6 +28,7 @@ import io.smallrye.mutiny.helpers.ParameterValidation; import io.vertx.mutiny.redis.client.Command; import io.vertx.mutiny.redis.client.Response; +import io.vertx.redis.client.ResponseType; class AbstractSortedSetCommands extends ReactiveSortable { @@ -733,22 +734,37 @@ protected String getScoreAsString(double score) { final List> decodeAsListOfScoredValues(Response response) { List> list = new ArrayList<>(); - - for (Response r : response) { - list.add(decodeAsScoredValue(r)); + if (response.iterator().next().type() == ResponseType.BULK) { + // Redis 5 + V current = null; + for (Response nested : response) { + if (current == null) { + current = decodeV(nested); + } else { + list.add(ScoredValue.of(current, nested.toDouble())); + current = null; + } + } + return list; + } else { + for (Response r : response) { + list.add(decodeAsScoredValue(r)); + } + return list; } - return list; } ScoredValue decodeAsScoredValue(Response r) { if (r == null || r.getDelegate() == null) { return null; } + if (r.size() == 0) { return ScoredValue.empty(); } return ScoredValue.of(decodeV(r.get(0)), r.get(1).toDouble()); + } Double decodeAsDouble(Response r) { diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java index dca44f726685c..3eb7b39df0228 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/Marshaller.java @@ -97,15 +97,30 @@ final T decode(Class clazz, byte[] r) { } public Map decodeAsMap(Response response, Class typeOfField, Class typeOfValue) { - if (response == null) { + if (response == null || response.size() == 0) { return Collections.emptyMap(); } Map map = new LinkedHashMap<>(); - for (Response member : response) { - for (String key : member.getKeys()) { - F field = decode(typeOfField, key.getBytes(StandardCharsets.UTF_8)); - V val = decode(typeOfValue, response.get(key)); - map.put(field, val); + if (response.iterator().next().type() == ResponseType.BULK) { + // Redis 5 + F current = null; // Just in case it's Redis 5. + for (Response member : response) { + if (current == null) { + current = decode(typeOfField, member.toString().getBytes(StandardCharsets.UTF_8)); + } else { + V val = decode(typeOfValue, member); + map.put(current, val); + current = null; + } + } + } else { + // MULTI - Redis 6+ + for (Response member : response) { + for (String key : member.getKeys()) { + F field = decode(typeOfField, key.getBytes(StandardCharsets.UTF_8)); + V val = decode(typeOfValue, response.get(key)); + map.put(field, val); + } } } return map; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/BloomCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/BloomCommandsTest.java index ed695c15437ad..4bfc034777d61 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/BloomCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/BloomCommandsTest.java @@ -14,6 +14,7 @@ import io.quarkus.redis.datasource.bloom.BloomCommands; import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; +@RequiresCommand("bf.add") public class BloomCommandsTest extends DatasourceTestBase { private RedisDataSource ds; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/CountMinCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/CountMinCommandsTest.java index 5c3f20a934559..b02bbc2b8550e 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/CountMinCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/CountMinCommandsTest.java @@ -15,6 +15,7 @@ import io.quarkus.redis.datasource.countmin.CountMinCommands; import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; +@RequiresCommand("cms.query") public class CountMinCommandsTest extends DatasourceTestBase { private RedisDataSource ds; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/CuckooCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/CuckooCommandsTest.java index be44f0cfad1c8..a90f502ef315f 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/CuckooCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/CuckooCommandsTest.java @@ -14,6 +14,7 @@ import io.quarkus.redis.datasource.cuckoo.CuckooCommands; import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; +@RequiresCommand("cf.add") public class CuckooCommandsTest extends DatasourceTestBase { private RedisDataSource ds; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/DatasourceTestBase.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/DatasourceTestBase.java index 4d7765cc83626..2ef0556e1e808 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/DatasourceTestBase.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/DatasourceTestBase.java @@ -1,65 +1,27 @@ package io.quarkus.redis.datasource; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.utility.DockerImageName; +import org.junit.jupiter.api.extension.ExtendWith; import io.vertx.mutiny.core.Vertx; -import io.vertx.mutiny.redis.client.Command; import io.vertx.mutiny.redis.client.Redis; import io.vertx.mutiny.redis.client.RedisAPI; -import io.vertx.mutiny.redis.client.Request; -import io.vertx.mutiny.redis.client.Response; +@ExtendWith(RedisServerExtension.class) public class DatasourceTestBase { final String key = UUID.randomUUID().toString(); - - public static Vertx vertx; - public static Redis redis; - public static RedisAPI api; - - static GenericContainer server = new GenericContainer<>( - DockerImageName.parse(System.getProperty("redis.base.image", "redis/redis-stack:7.0.2-RC2"))) - .withExposedPorts(6379); + static Redis redis; + static Vertx vertx; + static RedisAPI api; @BeforeAll static void init() { - vertx = Vertx.vertx(); - server.start(); - redis = Redis.createClient(vertx, "redis://" + server.getHost() + ":" + server.getFirstMappedPort()); - // If you want to use a local redis: redis = Redis.createClient(vertx, "redis://localhost:" + 6379); - api = RedisAPI.api(redis); - } - - @AfterAll - static void cleanup() { - redis.close(); - server.close(); - vertx.closeAndAwait(); - } - - public static List getAvailableCommands() { - List commands = new ArrayList<>(); - Response list = redis.send(Request.cmd(Command.COMMAND)).await().indefinitely(); - for (Response response : list) { - commands.add(response.get(0).toString()); - } - return commands; - - } - - public static String getRedisVersion() { - String info = redis.send(Request.cmd(Command.INFO)).await().indefinitely().toString(); - // Look for the redis_version line - return info.lines().filter(s -> s.startsWith("redis_version")).findAny() - .map(line -> line.split(":")[1]) - .orElseThrow(); + redis = RedisServerExtension.redis; + vertx = RedisServerExtension.vertx; + api = RedisServerExtension.api; } } diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/GeoCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/GeoCommandsTest.java index ea34b4ef21c71..beb076f528d75 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/GeoCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/GeoCommandsTest.java @@ -88,8 +88,13 @@ void geoadd() { added = geo.geoadd(key, GeoItem.of(Place.suze, SUZE_LONGITUDE, SUZE_LATITUDE)); assertThat(added).isTrue(); + } - added = geo.geoadd(key, GeoItem.of(new Place("foo", 1), CRUSSOL_LONGITUDE, CRUSSOL_LATITUDE), new GeoAddArgs().nx()); + @Test + @RequiresRedis6OrHigher + void geoaddWithNx() { + boolean added = geo.geoadd(key, GeoItem.of(new Place("foo", 1), CRUSSOL_LONGITUDE, CRUSSOL_LATITUDE), + new GeoAddArgs().nx()); assertThat(added).isTrue(); } @@ -106,6 +111,7 @@ void geoaddValue() { } @Test + @RequiresRedis6OrHigher void geoAddWithXXorCH() { boolean added = geo.geoadd(key, 44.9396, CRUSSOL_LATITUDE, Place.crussol, new GeoAddArgs().xx()); @@ -363,6 +369,7 @@ void georadiusbymember() { } @Test + @RequiresRedis6OrHigher void georadiusbymemberStoreDistWithCountAndSort() { populate(); String resultKey = key + "-2"; @@ -376,6 +383,19 @@ void georadiusbymemberStoreDistWithCountAndSort() { assertThat(dist.get(0).score()).isBetween(55d, 60d); } + @Test + void georadiusbymemberStoreDistWithSort() { + populate(); + String resultKey = key + "-2"; + long result = geo.georadiusbymember(key, Place.crussol, 100, GeoUnit.KM, + new GeoRadiusStoreArgs().descending().storeDistKey(resultKey)); + assertThat(result).isEqualTo(3); + + SortedSetCommands commands = ds.sortedSet(String.class); + List> dist = commands.zrangeWithScores(resultKey, 0, -1); + assertThat(dist).hasSize(3); + } + @Test void georadiusbymemberWithArgs() { populate(); @@ -442,6 +462,7 @@ void georadiusbymemberWithNullArgs() { } @Test + @RequiresRedis6OrHigher void geosearchWithCountAndSort() { populate(); @@ -460,6 +481,7 @@ void geosearchWithCountAndSort() { } @Test + @RequiresRedis6OrHigher void geosearchWithArgs() { populate(); @@ -508,6 +530,7 @@ void geosearchWithArgs() { } @Test + @RequiresRedis6OrHigher void geosearchStoreWithCountAndSort() { populate(); String resultKey = key + "-2"; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/GraphCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/GraphCommandsTest.java index 9d901d8ca9c59..6de00ca275841 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/GraphCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/GraphCommandsTest.java @@ -15,6 +15,7 @@ import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.vertx.core.json.JsonObject; +@RequiresCommand("graph.query") public class GraphCommandsTest extends DatasourceTestBase { private RedisDataSource ds; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/HashCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/HashCommandsTest.java index a5dd256e798c6..1c799b2e67e24 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/HashCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/HashCommandsTest.java @@ -11,6 +11,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -97,6 +98,22 @@ public void hgetall() { assertThat(hash.hgetall("missing")).isEmpty(); } + /** + * Reproducer for #28837. + */ + @Test + public void hgetallUsingIntegers() { + var cmd = ds.hash(Integer.class); + String key = UUID.randomUUID().toString(); + assertThat(cmd.hgetall(key).isEmpty()).isTrue(); + + cmd.hset(key, Map.of("a", 1, "b", 2, "c", 3)); + + Map map = cmd.hgetall(key); + + assertThat(map).hasSize(3); + } + @Test void hincrby() { assertThat(hash.hincrby(key, "one", 1)).isEqualTo(1); @@ -171,6 +188,7 @@ void hmsetWithNulls() { } @Test + @RequiresRedis7OrHigher void hrandfield() { hash.hset(key, Map.of("one", Person.person1, "two", Person.person2, "three", Person.person3)); @@ -179,6 +197,7 @@ void hrandfield() { } @Test + @RequiresRedis7OrHigher void hrandfieldWithValues() { Map map = Map.of("one", Person.person1, "two", Person.person2, "three", Person.person3); hash.hset(key, map); diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/JsonCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/JsonCommandsTest.java index e1332920cd2ee..73e68ea63ea14 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/JsonCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/JsonCommandsTest.java @@ -19,6 +19,7 @@ import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +@RequiresCommand("json.get") public class JsonCommandsTest extends DatasourceTestBase { private RedisDataSource ds; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/KeyCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/KeyCommandsTest.java index 90949c1cfc581..4c68e0fa65b57 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/KeyCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/KeyCommandsTest.java @@ -75,6 +75,7 @@ void unlink() { } @Test + @RequiresRedis6OrHigher void copy() { values.set(key, Person.person7); assertThat(keys.copy(key, key + "2")).isTrue(); @@ -83,6 +84,7 @@ void copy() { } @Test + @RequiresRedis6OrHigher void copyWithReplace() { values.set(key, Person.person7); values.set(key + 2, Person.person1); @@ -91,6 +93,7 @@ void copyWithReplace() { } @Test + @RequiresRedis6OrHigher void copyWithDestinationDb() { ds.withConnection(connection -> { connection.value(String.class, Person.class).set(key, Person.person7); @@ -398,6 +401,7 @@ void scanWithArgs() { } @Test + @RequiresRedis6OrHigher void scanWithType() { values.set("key1", Person.person7); ds.list(Person.class).lpush("key2", Person.person7); diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ListCommandTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ListCommandTest.java index c05422f3a2a36..d07f0b84e823b 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ListCommandTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ListCommandTest.java @@ -157,6 +157,7 @@ void lmpopMany() { } @Test + @RequiresRedis6OrHigher void lpopCount() { assertThat(lists.lpop(key, 1)).isEqualTo(List.of()); lists.rpush(key, Person.person1, Person.person2); @@ -164,6 +165,7 @@ void lpopCount() { } @Test + @RequiresRedis6OrHigher void lpos() { lists.rpush(key, Person.person4, Person.person5, Person.person6, Person.person1, Person.person2, Person.person3, @@ -259,6 +261,7 @@ void rpop() { } @Test + @RequiresRedis6OrHigher void rpopCount() { assertThat(lists.rpop(key, 1)).isEqualTo(List.of()); lists.rpush(key, Person.person1, Person.person2); @@ -301,6 +304,7 @@ void rpushxMultiple() { } @Test + @RequiresRedis6OrHigher void lmove() { String list1 = key; String list2 = key + "-2"; @@ -313,6 +317,7 @@ void lmove() { } @Test + @RequiresRedis6OrHigher void blmove() { String list1 = key; String list2 = key + "-2"; @@ -325,6 +330,7 @@ void blmove() { } @Test + @RequiresRedis6OrHigher void sort() { ListCommands commands = ds.list(String.class, String.class); commands.rpush(key, "9", "5", "1", "3", "5", "8", "7", "6", "2", "4"); diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/Redis6OrHigherCondition.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/Redis6OrHigherCondition.java new file mode 100644 index 0000000000000..9f21573655845 --- /dev/null +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/Redis6OrHigherCondition.java @@ -0,0 +1,35 @@ +package io.quarkus.redis.datasource; + +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled; +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled; + +import java.util.Optional; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.util.AnnotationUtils; + +class Redis6OrHigherCondition implements ExecutionCondition { + + private static final ConditionEvaluationResult ENABLED_BY_DEFAULT = enabled("@RequiresRedis6OrHigher is not present"); + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Optional optional = AnnotationUtils.findAnnotation(context.getElement(), + RequiresRedis6OrHigher.class); + + if (optional.isPresent()) { + String version = RedisServerExtension.getRedisVersion(); + + return isRedis6orHigher(version) ? enabled("Redis " + version + " >= 6") + : disabled("Disabled, Redis " + version + " < 6"); + } + + return ENABLED_BY_DEFAULT; + } + + public static boolean isRedis6orHigher(String version) { + return Integer.parseInt(version.split("\\.")[0]) >= 6; + } +} diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/Redis7OrHigherCondition.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/Redis7OrHigherCondition.java index bcdbc6a6fb5f4..96c63ed4cffec 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/Redis7OrHigherCondition.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/Redis7OrHigherCondition.java @@ -20,7 +20,7 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con RequiresRedis7OrHigher.class); if (optional.isPresent()) { - String version = DatasourceTestBase.getRedisVersion(); + String version = RedisServerExtension.getRedisVersion(); return isRedis7orHigher(version) ? enabled("Redis " + version + " >= 7") : disabled("Disabled, Redis " + version + " < 7"); diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RedisCommandCondition.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RedisCommandCondition.java index 4bbb799ccf909..d3bd0f464041c 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RedisCommandCondition.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RedisCommandCondition.java @@ -22,7 +22,7 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con if (optional.isPresent()) { String[] cmd = optional.get().value(); - List commands = DatasourceTestBase.getAvailableCommands(); + List commands = RedisServerExtension.getAvailableCommands(); for (String c : cmd) { if (!commands.contains(c.toLowerCase())) { diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RedisServerExtension.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RedisServerExtension.java new file mode 100644 index 0000000000000..387beb2c6c6f1 --- /dev/null +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RedisServerExtension.java @@ -0,0 +1,93 @@ +package io.quarkus.redis.datasource; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.redis.client.Command; +import io.vertx.mutiny.redis.client.Redis; +import io.vertx.mutiny.redis.client.RedisAPI; +import io.vertx.mutiny.redis.client.Request; +import io.vertx.mutiny.redis.client.Response; + +@SuppressWarnings("resource") +public class RedisServerExtension implements BeforeAllCallback, AfterAllCallback { + + static GenericContainer server = new GenericContainer<>( + DockerImageName.parse(System.getProperty("redis.base.image", "redis/redis-stack:7.0.2-RC2"))) + .withExposedPorts(6379); + static Redis redis; + static RedisAPI api; + static Vertx vertx; + + public static String getHost() { + return server.getHost(); + } + + public static Integer getFirstMappedPort() { + return server.getFirstMappedPort(); + } + + @Override + public void beforeAll(ExtensionContext extensionContext) { + init(); + } + + private static boolean init() { + if (!server.isRunning()) { + server.start(); + vertx = Vertx.vertx(); + redis = Redis.createClient(vertx, + "redis://" + RedisServerExtension.getHost() + ":" + RedisServerExtension.getFirstMappedPort()); + // If you want to use a local redis: redis = Redis.createClient(vertx, "redis://localhost:" + 6379); + api = RedisAPI.api(redis); + return true; + } + return false; + } + + @Override + public void afterAll(ExtensionContext extensionContext) { + cleanup(); + } + + private static void cleanup() { + redis.close(); + vertx.closeAndAwait(); + server.stop(); + } + + public static List getAvailableCommands() { + boolean mustcleanup = init(); + List commands = new ArrayList<>(); + Response list = redis.send(Request.cmd(Command.COMMAND)).await().indefinitely(); + for (Response response : list) { + commands.add(response.get(0).toString()); + } + if (mustcleanup) { + cleanup(); + } + return commands; + + } + + public static String getRedisVersion() { + boolean mustcleanup = init(); + String info = redis.send(Request.cmd(Command.INFO)).await().indefinitely().toString(); + if (mustcleanup) { + cleanup(); + } + // Look for the redis_version line + return info.lines().filter(s -> s.startsWith("redis_version")).findAny() + .map(line -> line.split(":")[1]) + .orElseThrow(); + + } + +} diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RequiresCommand.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RequiresCommand.java index b06160555ba15..d0117ae553b69 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RequiresCommand.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RequiresCommand.java @@ -8,7 +8,7 @@ import org.junit.jupiter.api.extension.ExtendWith; -@Target(ElementType.METHOD) +@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Inherited @ExtendWith(RedisCommandCondition.class) diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RequiresRedis6OrHigher.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RequiresRedis6OrHigher.java new file mode 100644 index 0000000000000..3408440f5b592 --- /dev/null +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/RequiresRedis6OrHigher.java @@ -0,0 +1,18 @@ +package io.quarkus.redis.datasource; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@ExtendWith(Redis6OrHigherCondition.class) +@interface RequiresRedis6OrHigher { + + // Important the class must extend DatasourceTestBase. +} diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SetCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SetCommandsTest.java index f862478a92939..9a11440bdc311 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SetCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SetCommandsTest.java @@ -114,6 +114,7 @@ void smembers() { } @Test + @RequiresRedis6OrHigher void smismember() { assertThat(sets.smismember(key, person1)).isEqualTo(List.of(false)); sets.sadd(key, person1); @@ -309,6 +310,7 @@ private void populate() { } @Test + @RequiresRedis6OrHigher void sort() { SetCommands commands = ds.set(String.class, String.class); commands.sadd(key, "9", "5", "1", "3", "5", "8", "7", "6", "2", "4"); diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java index b70c037f86ff7..bb59e5f8acd19 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SortedSetCommandsTest.java @@ -104,6 +104,7 @@ void zaddxx() { } @Test + @RequiresRedis6OrHigher void zaddch() { assertThat(setOfPlaces.zadd(key, 1.0, Place.crussol)).isTrue(); assertThat(setOfPlaces.zadd(key, new ZAddArgs().ch().xx(), 2.0, Place.crussol)).isTrue(); @@ -114,6 +115,7 @@ void zaddch() { } @Test + @RequiresRedis6OrHigher void zaddincr() { assertThat(setOfPlaces.zadd(key, 1.0, Place.crussol)).isTrue(); assertThat(setOfPlaces.zaddincr(key, 2.0, Place.crussol)).isEqualTo(3.0); @@ -138,6 +140,7 @@ void zaddincrxx() { } @Test + @RequiresRedis6OrHigher void zaddgt() { assertThat(setOfPlaces.zadd(key, 1.0, Place.crussol)).isTrue(); // new score less than the current score @@ -155,6 +158,7 @@ void zaddgt() { } @Test + @RequiresRedis6OrHigher void zaddlt() { assertThat(setOfPlaces.zadd(key, 2.0, Place.crussol)).isTrue(); // new score greater than the current score @@ -195,6 +199,7 @@ void zcount() { } @Test + @RequiresRedis6OrHigher void zdiff() { String zset1 = "zset1"; String zset2 = "zset2"; @@ -213,6 +218,7 @@ void zdiff() { } @Test + @RequiresRedis6OrHigher void zdiffstore() { String zset1 = "zset1"; String zset2 = "zset2"; @@ -255,6 +261,7 @@ void zinterstore() { } @Test + @RequiresRedis6OrHigher void zinterstoreWithArgs() { setOfPlaces.zadd("zset1", Map.of(Place.crussol, 1.0, Place.grignan, 2.0)); setOfPlaces.zadd("zset2", Map.of(Place.crussol, 2.0, Place.grignan, 3.0, Place.suze, 4.0)); @@ -301,6 +308,7 @@ void zpopmax() { } @Test + @RequiresRedis6OrHigher void zrandmember() { setOfPlaces.zadd("zset", Map.of(Place.crussol, 2.0, Place.grignan, 3.0, Place.suze, 4.0)); assertThat(setOfPlaces.zrandmember("zset")).isIn(Place.crussol, Place.grignan, Place.suze); @@ -326,6 +334,7 @@ void zrangeWithScores() { } @Test + @RequiresRedis6OrHigher void zrangebyscore() { setOfPlaces.zadd(key, Map.of(Place.crussol, 1.0, Place.grignan, 2.0, Place.suze, 3.0, Place.adhemar, 4.0)); @@ -343,6 +352,7 @@ void zrangebyscore() { } @Test + @RequiresRedis6OrHigher void zrangebyscoreWithScores() { setOfPlaces.zadd(key, Map.of(Place.crussol, 1.0, Place.grignan, 2.0, Place.suze, 3.0, Place.adhemar, 4.0)); @@ -366,6 +376,7 @@ void zrangebyscoreWithScores() { } @Test + @RequiresRedis6OrHigher void zrangebyscoreWithScoresInfinity() { setOfPlaces.zadd(key, Map.of(Place.crussol, Double.POSITIVE_INFINITY, Place.grignan, Double.NEGATIVE_INFINITY)); assertThat(setOfPlaces.zrangebyscoreWithScores(key, new ScoreRange<>(null, null))).hasSize(2); @@ -373,6 +384,7 @@ void zrangebyscoreWithScoresInfinity() { } @Test + @RequiresRedis6OrHigher void zrangestorebylex() { setOfStrings.zadd(key, Map.of("a", 1.0, "b", 2.0, "c", 3.0, "d", 4.0)); assertThat(setOfStrings.zrangestorebylex("key1", key, new Range<>("b", "d"), new ZRangeArgs().limit(0, 4))) @@ -384,6 +396,7 @@ void zrangestorebylex() { } @Test + @RequiresRedis6OrHigher void zrangestorebyscore() { setOfPlaces.zadd(key, Map.of(Place.crussol, 1.0, Place.grignan, 2.0, Place.suze, 3.0, Place.adhemar, 4.0)); assertThat(setOfPlaces.zrangestorebyscore("key1", key, new ScoreRange<>(0.0, 2.0), @@ -395,6 +408,7 @@ void zrangestorebyscore() { } @Test + @RequiresRedis6OrHigher void zrangestore() { setOfPlaces.zadd(key, Map.of(Place.crussol, 1.0, Place.grignan, 2.0, Place.suze, 3.0, Place.adhemar, 4.0)); assertThat(setOfPlaces.zrangestore("key1", key, 0, -1)).isEqualTo(4); @@ -447,6 +461,7 @@ void zremrangebyrank() { } @Test + @RequiresRedis6OrHigher void zrevrange() { populate(); assertThat(setOfPlaces.zrange(key, 0, -1, new ZRangeArgs().rev())) @@ -454,6 +469,7 @@ void zrevrange() { } @Test + @RequiresRedis6OrHigher void zrevrangeWithScores() { populate(); assertThat(setOfPlaces.zrangeWithScores(key, 0, -1, new ZRangeArgs().rev())) @@ -462,6 +478,7 @@ void zrevrangeWithScores() { } @Test + @RequiresRedis6OrHigher void zrevrangebylex() { populateManyStringEntries(); assertThat(setOfStrings.zrangebylex(key, Range.unbounded(), new ZRangeArgs().rev())).hasSize(100); @@ -474,6 +491,7 @@ void zrevrangebylex() { } @Test + @RequiresRedis6OrHigher void zrevrangebyscore() { setOfPlaces.zadd(key, Map.of(Place.crussol, 1.0, Place.grignan, 2.0, Place.suze, 3.0, Place.adhemar, 4.0)); ZRangeArgs rev = new ZRangeArgs().rev(); @@ -492,6 +510,7 @@ void zrevrangebyscore() { } @Test + @RequiresRedis6OrHigher void zrevrangebyscoreWithScores() { setOfPlaces.zadd(key, Map.of(Place.crussol, 1.0, Place.grignan, 2.0, Place.suze, 3.0, Place.adhemar, 4.0)); ZRangeArgs rev = new ZRangeArgs().rev(); @@ -523,6 +542,7 @@ void zrevrank() { } @Test + @RequiresRedis6OrHigher void zrevrangestorebylex() { setOfStrings.zadd(key, Map.of("a", 1.0, "b", 2.0, "c", 3.0, "d", 4.0)); assertThat(setOfStrings.zrangestorebylex("key1", key, new Range<>("c", "-"), @@ -531,6 +551,7 @@ void zrevrangestorebylex() { } @Test + @RequiresRedis6OrHigher void zrevrangestorebyscore() { setOfPlaces.zadd(key, Map.of(Place.crussol, 1.0, Place.grignan, 2.0, Place.suze, 3.0, Place.adhemar, 4.0)); assertThat( @@ -728,6 +749,7 @@ void zlexcount() { } @Test + @RequiresRedis6OrHigher public void zmscore() { setOfPlaces.zadd("zset1", Map.of(Place.crussol, 1.0, Place.grignan, 2.0)); assertThat(setOfPlaces.zmscore("zset1", Place.crussol, Place.suze, Place.grignan)) @@ -821,6 +843,7 @@ public void bzmpopMax() { } @Test + @RequiresRedis6OrHigher void zrangebylex() { populateManyStringEntries(); @@ -836,6 +859,7 @@ void zrangebylex() { } @Test + @RequiresRedis6OrHigher void zremrangebylex() { populateManyStringEntries(); assertThat(setOfStrings.zremrangebylex(key, new Range<>("aaa", false, "zzz", true))).isEqualTo(100); @@ -845,6 +869,7 @@ void zremrangebylex() { } @Test + @RequiresRedis6OrHigher void zunion() { String zset1 = "zset1"; String zset2 = "zset2"; @@ -868,6 +893,7 @@ void zunion() { } @Test + @RequiresRedis6OrHigher void zinter() { String zset1 = "zset1"; String zset2 = "zset2"; @@ -887,6 +913,7 @@ void zinter() { } @Test + @RequiresRedis6OrHigher void zinterWithScores() { String zset1 = "zset1"; String zset2 = "zset2"; @@ -904,6 +931,7 @@ void zinterWithScores() { } @Test + @RequiresRedis6OrHigher void zinterWithArgs() { String zset1 = "zset1"; String zset2 = "zset2"; @@ -931,6 +959,7 @@ private void populateManyStringEntries() { } @Test + @RequiresRedis6OrHigher void sort() { SortedSetCommands commands = ds.sortedSet(String.class, String.class); commands.zadd(key, Map.of("9", 9.0, "1", 1.0, "3", 3.0, "5", 5.0, diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StringCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StringCommandsTest.java index 3e0e16ca628eb..120f580c58181 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StringCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/StringCommandsTest.java @@ -22,6 +22,7 @@ import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; @SuppressWarnings("deprecation") +@RequiresRedis6OrHigher // The ValueCommandsTest verify the behavior with Redis 5 public class StringCommandsTest extends DatasourceTestBase { private RedisDataSource ds; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TopKCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TopKCommandsTest.java index 016ca82905a3a..0f952ff6fd29c 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TopKCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TopKCommandsTest.java @@ -14,6 +14,7 @@ import io.quarkus.redis.datasource.topk.TopKCommands; import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; +@RequiresCommand("topk.add") public class TopKCommandsTest extends DatasourceTestBase { private RedisDataSource ds; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalBloomCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalBloomCommandsTest.java index ce9fecdf1ae71..8982f9f50560f 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalBloomCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalBloomCommandsTest.java @@ -17,6 +17,7 @@ import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; @SuppressWarnings("unchecked") +@RequiresCommand("bf.add") public class TransactionalBloomCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalCountMinCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalCountMinCommandsTest.java index 6ea305b5c35ed..de133ca981e90 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalCountMinCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalCountMinCommandsTest.java @@ -17,6 +17,7 @@ import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; @SuppressWarnings({ "unchecked", "ConstantConditions" }) +@RequiresCommand("cms.query") public class TransactionalCountMinCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalCuckooCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalCuckooCommandsTest.java index be8a1cc7b31ef..53ee325df57d2 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalCuckooCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalCuckooCommandsTest.java @@ -17,6 +17,7 @@ import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; @SuppressWarnings("unchecked") +@RequiresCommand("cf.add") public class TransactionalCuckooCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalGeoCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalGeoCommandsTest.java index 503de2254a51a..9422bc9da9505 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalGeoCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalGeoCommandsTest.java @@ -20,6 +20,7 @@ import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; +@RequiresRedis6OrHigher public class TransactionalGeoCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalGraphCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalGraphCommandsTest.java index eb40c8a9b4f2d..bdd13499510d1 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalGraphCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalGraphCommandsTest.java @@ -18,6 +18,7 @@ import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; @SuppressWarnings("unchecked") +@RequiresCommand("graph.query") public class TransactionalGraphCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalHashCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalHashCommandsTest.java index 76145f2f90d1e..786a90799428a 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalHashCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalHashCommandsTest.java @@ -14,6 +14,7 @@ import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; +@RequiresRedis6OrHigher public class TransactionalHashCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalHyperLogLogCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalHyperLogLogCommandsTest.java index 13d77c1b66a33..7c265414d3aae 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalHyperLogLogCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalHyperLogLogCommandsTest.java @@ -14,6 +14,7 @@ import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; +@RequiresRedis6OrHigher public class TransactionalHyperLogLogCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalJsonCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalJsonCommandsTest.java index 9944b1764c83e..ad50d48e42423 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalJsonCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalJsonCommandsTest.java @@ -19,6 +19,7 @@ import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +@RequiresCommand("json.get") public class TransactionalJsonCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalKeyTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalKeyTest.java index 4043004f019c6..9c11473c8c7a6 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalKeyTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalKeyTest.java @@ -18,6 +18,7 @@ import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; +@RequiresRedis6OrHigher public class TransactionalKeyTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalListCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalListCommandsTest.java index 3d00167c5dfd5..61fa8059007e5 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalListCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalListCommandsTest.java @@ -14,6 +14,7 @@ import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; +@RequiresRedis6OrHigher public class TransactionalListCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalSetCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalSetCommandsTest.java index 24c172c2c4dfe..5a2925243e498 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalSetCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalSetCommandsTest.java @@ -14,6 +14,7 @@ import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; +@RequiresRedis6OrHigher public class TransactionalSetCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalSortedSetCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalSortedSetCommandsTest.java index bfae930579803..b1ef77d5ba07d 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalSortedSetCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalSortedSetCommandsTest.java @@ -17,6 +17,7 @@ import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; +@RequiresRedis6OrHigher public class TransactionalSortedSetCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStringCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStringCommandsTest.java index c9d8f6864fde6..35c2088fdea01 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStringCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalStringCommandsTest.java @@ -16,6 +16,7 @@ import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; @SuppressWarnings("deprecation") +@RequiresRedis6OrHigher public class TransactionalStringCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalTopKCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalTopKCommandsTest.java index eedd37d320668..1ad2f1a74d359 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalTopKCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalTopKCommandsTest.java @@ -18,6 +18,7 @@ import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; @SuppressWarnings({ "unchecked", "ConstantConditions" }) +@RequiresCommand("topk.add") public class TransactionalTopKCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalValueCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalValueCommandsTest.java index 7745073b19ba4..735bd3b8e390a 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalValueCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/TransactionalValueCommandsTest.java @@ -15,6 +15,7 @@ import io.quarkus.redis.runtime.datasource.BlockingRedisDataSourceImpl; import io.quarkus.redis.runtime.datasource.ReactiveRedisDataSourceImpl; +@RequiresRedis6OrHigher public class TransactionalValueCommandsTest extends DatasourceTestBase { private RedisDataSource blocking; diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ValueCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ValueCommandsTest.java index fff497be12008..4fab036f15263 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ValueCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/ValueCommandsTest.java @@ -68,6 +68,7 @@ void getbit() { } @Test + @RequiresRedis6OrHigher void getdel() { values.set(key, value); assertThat(values.getdel(key)).isEqualTo(value); @@ -75,6 +76,7 @@ void getdel() { } @Test + @RequiresRedis6OrHigher void getex() { values.set(key, value); assertThat(values.getex(key, new GetExArgs().ex(Duration.ofSeconds(100)))).isEqualTo(value); @@ -176,6 +178,7 @@ void set() { } @Test + @RequiresRedis6OrHigher void setExAt() { KeyCommands keys = ds.key(String.class); @@ -187,6 +190,7 @@ void setExAt() { } @Test + @RequiresRedis6OrHigher void setKeepTTL() { KeyCommands keys = ds.key(String.class); @@ -207,6 +211,7 @@ void setNegativePX() { } @Test + @RequiresRedis7OrHigher void setGet() { assertThat(values.setGet(key, value)).isNull(); assertThat(values.setGet(key, "value2")).isEqualTo(value); @@ -214,6 +219,7 @@ void setGet() { } @Test + @RequiresRedis7OrHigher void setGetWithArgs() { KeyCommands keys = ds.key(String.class); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/MultipartOutputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/MultipartOutputTest.java index 3a4e2977ed6d0..0a6a7cfcd8f2b 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/MultipartOutputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/MultipartOutputTest.java @@ -38,7 +38,6 @@ public void testSimple() { .then() .contentType(ContentType.MULTIPART) .statusCode(200) - .log().all() .extract().asString(); assertContains(response, "name", MediaType.TEXT_PLAIN, EXPECTED_RESPONSE_NAME); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/ApplicationCoroutineScope.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/ApplicationCoroutineScope.kt index 8f8f76acd7b08..5616f89ed3ab7 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/ApplicationCoroutineScope.kt +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/ApplicationCoroutineScope.kt @@ -42,7 +42,6 @@ class VertxDispatcher(private val vertxContext: Context, private val requestScop try { block.run() } finally { - CurrentRequestManager.set(null) requestScope.deactivate() } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/FormDataOutputMapperGenerator.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/FormDataOutputMapperGenerator.java deleted file mode 100644 index e591725ae5ed6..0000000000000 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/FormDataOutputMapperGenerator.java +++ /dev/null @@ -1,265 +0,0 @@ -package io.quarkus.resteasy.reactive.server.deployment; - -import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.FORM_PARAM; -import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_FORM_PARAM; - -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; - -import javax.ws.rs.core.MediaType; - -import org.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationValue; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.DotName; -import org.jboss.jandex.FieldInfo; -import org.jboss.jandex.IndexView; -import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.Type; -import org.jboss.logging.Logger; -import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; -import org.jboss.resteasy.reactive.server.core.multipart.MultipartMessageBodyWriter; -import org.jboss.resteasy.reactive.server.core.multipart.MultipartOutputInjectionTarget; -import org.jboss.resteasy.reactive.server.core.multipart.PartItem; - -import io.quarkus.deployment.bean.JavaBeanUtil; -import io.quarkus.gizmo.AssignableResultHandle; -import io.quarkus.gizmo.ClassCreator; -import io.quarkus.gizmo.ClassOutput; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; - -final class FormDataOutputMapperGenerator { - - private static final Logger LOGGER = Logger.getLogger(FormDataOutputMapperGenerator.class); - - private static final String TRANSFORM_METHOD_NAME = "mapFrom"; - private static final String ARRAY_LIST_ADD_METHOD_NAME = "add"; - - private FormDataOutputMapperGenerator() { - } - - /** - * Returns true whether the returning type uses either {@link org.jboss.resteasy.reactive.RestForm} - * or {@link org.jboss.resteasy.reactive.server.core.multipart.FormData} annotations. - */ - public static boolean isReturnTypeCompatible(ClassInfo returnTypeClassInfo, IndexView index) { - // go up the class hierarchy until we reach Object - ClassInfo currentClassInHierarchy = returnTypeClassInfo; - while (true) { - List fields = currentClassInHierarchy.fields(); - for (FieldInfo field : fields) { - if (Modifier.isStatic(field.flags())) { // nothing we need to do about static fields - continue; - } - - if (field.annotation(REST_FORM_PARAM) != null || field.annotation(FORM_PARAM) != null) { - // Found either @RestForm or @FormParam in returning class, it's compatible. - return true; - } - } - - DotName superClassDotName = currentClassInHierarchy.superName(); - if (superClassDotName.equals(DotNames.OBJECT_NAME)) { - break; - } - ClassInfo newCurrentClassInHierarchy = index.getClassByName(superClassDotName); - if (newCurrentClassInHierarchy == null) { - printWarningMessageForMissingJandexIndex(currentClassInHierarchy, superClassDotName); - break; - } - - currentClassInHierarchy = newCurrentClassInHierarchy; - } - - // if we reach this point then the returning type is not compatible. - return false; - } - - /** - * Generates a class that map a Pojo into {@link PartItem} that is then used by {@link MultipartMessageBodyWriter}. - * - *

- * For example for a pojo like: - * - *

-     * public class FormData {
-     *
-     *     @RestForm
-     *     @PartType(MediaType.TEXT_PLAIN)
-     *     private String text;
-     *
-     *     @RestForm
-     *     @PartType(MediaType.APPLICATION_OCTET_STREAM)
-     *     public File file;
-     *
-     *     public String getText() {
-     *         return text;
-     *     }
-     *
-     *     public void setText(String text) {
-     *         this.text = text;
-     *     }
-     *
-     *     public File getFile() {
-     *         return file;
-     *     }
-     *
-     *     public void setFile(File file) {
-     *         this.file = file;
-     *     }
-     * }
-     * 
- * - *

- * - * The generated mapper would look like: - * - *

-     * public class FormData_generated_mapper implements MultipartOutputInjectionTarget {
-     *
-     *     public FormDataOutput mapFrom(Object var1) {
-     *         FormDataOutput var2 = new FormDataOutput();
-     *         FormData var4 = (FormData) var1;
-     *         File var3 = var4.data;
-     *         MultipartSupport.addPartItemToFormDataOutput(var2, "file", "application/octet-stream", var3);
-     *         File var5 = var4.text;
-     *         MultipartSupport.addPartItemToFormDataOutput(var2, "text", "text/plain", var5);
-     *         return var2;
-     *     }
-     * }
-     * 
- */ - static String generate(ClassInfo returnTypeClassInfo, ClassOutput classOutput, IndexView index) { - String returnClassName = returnTypeClassInfo.name().toString(); - String generateClassName = MultipartMessageBodyWriter.getGeneratedMapperClassNameFor(returnClassName); - String interfaceClassName = MultipartOutputInjectionTarget.class.getName(); - try (ClassCreator cc = new ClassCreator(classOutput, generateClassName, null, Object.class.getName(), - interfaceClassName)) { - MethodCreator populate = cc.getMethodCreator(TRANSFORM_METHOD_NAME, List.class.getName(), - Object.class); - populate.setModifiers(Modifier.PUBLIC); - - ResultHandle listPartItemListInstanceHandle = populate.newInstance(MethodDescriptor.ofConstructor(ArrayList.class)); - ResultHandle inputInstanceHandle = populate.checkCast(populate.getMethodParam(0), returnClassName); - - // go up the class hierarchy until we reach Object - ClassInfo currentClassInHierarchy = returnTypeClassInfo; - while (true) { - List fields = currentClassInHierarchy.fields(); - for (FieldInfo field : fields) { - if (Modifier.isStatic(field.flags())) { // nothing we need to do about static fields - continue; - } - - AnnotationInstance formParamInstance = field.annotation(REST_FORM_PARAM); - if (formParamInstance == null) { - formParamInstance = field.annotation(FORM_PARAM); - } - if (formParamInstance == null) { // fields not annotated with @RestForm or @FormParam are completely ignored - continue; - } - - boolean useFieldAccess = false; - String getterName = JavaBeanUtil.getGetterName(field.name(), field.type().name()); - Type fieldType = field.type(); - DotName fieldDotName = fieldType.name(); - MethodInfo getter = currentClassInHierarchy.method(getterName); - if (getter == null) { - // even if the field is private, it will be transformed to be made public - useFieldAccess = true; - } - if (!useFieldAccess && !Modifier.isPublic(getter.flags())) { - throw new IllegalArgumentException( - "Getter '" + getterName + "' of class '" + returnTypeClassInfo + "' must be public"); - } - - String formAttrName = field.name(); - AnnotationValue formParamValue = formParamInstance.value(); - if (formParamValue != null) { - formAttrName = formParamValue.asString(); - } - - // TODO: not sure if this is correct, but it seems to be what RESTEasy does and it also makes most sense in the context of a POJO - String partType = MediaType.TEXT_PLAIN; - AnnotationInstance partTypeInstance = field.annotation(ResteasyReactiveDotNames.PART_TYPE_NAME); - if (partTypeInstance != null) { - AnnotationValue partTypeValue = partTypeInstance.value(); - if (partTypeValue != null) { - partType = partTypeValue.asString(); - } - } - - // Cast part type to MediaType. - AssignableResultHandle partTypeHandle = populate.createVariable(MediaType.class); - populate.assign(partTypeHandle, - populate.invokeStaticMethod( - MethodDescriptor.ofMethod(MediaType.class, "valueOf", MediaType.class, String.class), - populate.load(partType))); - - // Continue with the value - AssignableResultHandle resultHandle = populate.createVariable(Object.class); - - if (useFieldAccess) { - populate.assign(resultHandle, - populate.readInstanceField( - FieldDescriptor.of(currentClassInHierarchy.name().toString(), field.name(), - fieldDotName.toString()), - inputInstanceHandle)); - } else { - populate.assign(resultHandle, - populate.invokeVirtualMethod( - MethodDescriptor.ofMethod(currentClassInHierarchy.name().toString(), - getterName, fieldDotName.toString()), - inputInstanceHandle)); - } - - // Get parameterized type if field type is a parameterized class - String firstParamType = ""; - if (fieldType.kind() == Type.Kind.PARAMETERIZED_TYPE) { - List argumentTypes = fieldType.asParameterizedType().arguments(); - if (argumentTypes.size() > 0) { - firstParamType = argumentTypes.get(0).name().toString(); - } - } - - // Create Part Item instance - ResultHandle partItemInstanceHandle = populate.newInstance( - MethodDescriptor.ofConstructor(PartItem.class, - String.class, MediaType.class, Object.class, String.class), - populate.load(formAttrName), partTypeHandle, resultHandle, populate.load(firstParamType)); - - // Add it to the list - populate.invokeVirtualMethod( - MethodDescriptor.ofMethod(ArrayList.class, ARRAY_LIST_ADD_METHOD_NAME, boolean.class, Object.class), - listPartItemListInstanceHandle, partItemInstanceHandle); - } - - DotName superClassDotName = currentClassInHierarchy.superName(); - if (superClassDotName.equals(DotNames.OBJECT_NAME)) { - break; - } - ClassInfo newCurrentClassInHierarchy = index.getClassByName(superClassDotName); - if (newCurrentClassInHierarchy == null) { - printWarningMessageForMissingJandexIndex(currentClassInHierarchy, superClassDotName); - break; - } - currentClassInHierarchy = newCurrentClassInHierarchy; - } - - populate.returnValue(listPartItemListInstanceHandle); - } - return generateClassName; - } - - private static void printWarningMessageForMissingJandexIndex(ClassInfo currentClassInHierarchy, DotName superClassDotName) { - if (!superClassDotName.toString().startsWith("java.")) { - LOGGER.warn("Class '" + superClassDotName + "' which is a parent class of '" - + currentClassInHierarchy.name() - + "' is not part of the Jandex index so its fields will be ignored. If you intended to include these fields, consider making the dependency part of the Jandex index by following the advice at: https://quarkus.io/guides/cdi-reference#bean_discovery"); - } - } -} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusMultipartReturnTypeHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusMultipartReturnTypeHandler.java index 6c3c095096f20..5c309395931ee 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusMultipartReturnTypeHandler.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/QuarkusMultipartReturnTypeHandler.java @@ -11,6 +11,7 @@ import org.jboss.resteasy.reactive.common.processor.AdditionalWriters; import org.jboss.resteasy.reactive.common.processor.EndpointIndexer; import org.jboss.resteasy.reactive.server.core.multipart.MultipartMessageBodyWriter; +import org.jboss.resteasy.reactive.server.processor.generation.multipart.FormDataOutputMapperGenerator; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java index 6ebcdeab6174c..f1ce3eb26dd5e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputResource.java @@ -11,6 +11,7 @@ import javax.ws.rs.core.MediaType; import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.core.multipart.MultipartFormDataOutput; @Path("/multipart/output") public class MultipartOutputResource { @@ -51,6 +52,20 @@ public RestResponse restResponse() { return RestResponse.ResponseBuilder.ok(response).header("foo", "bar").build(); } + @GET + @Path("/with-form-data") + @Produces(MediaType.MULTIPART_FORM_DATA) + public RestResponse withFormDataOutput() { + MultipartFormDataOutput form = new MultipartFormDataOutput(); + form.addFormData("name", RESPONSE_NAME, MediaType.TEXT_PLAIN_TYPE); + form.addFormData("custom-surname", RESPONSE_SURNAME, MediaType.TEXT_PLAIN_TYPE); + form.addFormData("custom-status", RESPONSE_STATUS, MediaType.TEXT_PLAIN_TYPE) + .getHeaders().putSingle("extra-header", "extra-value"); + form.addFormData("values", RESPONSE_VALUES, MediaType.TEXT_PLAIN_TYPE); + form.addFormData("active", RESPONSE_ACTIVE, MediaType.TEXT_PLAIN_TYPE); + return RestResponse.ResponseBuilder.ok(form).header("foo", "bar").build(); + } + @GET @Path("/string") @Produces(MediaType.MULTIPART_FORM_DATA) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java index 5795599df878c..3e8bb225da17f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java @@ -63,6 +63,22 @@ public void testRestResponse() { assertContainsValue(response, "num", MediaType.TEXT_PLAIN, "0"); } + @Test + public void testWithFormData() { + String response = RestAssured.get("/multipart/output/with-form-data") + .then() + .log().all() + .contentType(ContentType.MULTIPART) + .statusCode(200) + .extract().asString(); + + assertContainsValue(response, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); + assertContainsValue(response, "custom-surname", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_SURNAME); + assertContainsValue(response, "custom-status", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_STATUS); + assertContainsValue(response, "active", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_ACTIVE); + assertContainsValue(response, "values", MediaType.TEXT_PLAIN, "[one, two]"); + } + @Test public void testString() { RestAssured.get("/multipart/output/string") diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusContextProducers.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusContextProducers.java index 07bbcb5aae75e..1f9b04bbbba7a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusContextProducers.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/QuarkusContextProducers.java @@ -1,16 +1,19 @@ package io.quarkus.resteasy.reactive.server.runtime; +import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.RequestScoped; import javax.enterprise.inject.Produces; import javax.inject.Singleton; +import javax.ws.rs.ext.Providers; import org.jboss.resteasy.reactive.server.core.CurrentRequestManager; +import org.jboss.resteasy.reactive.server.jaxrs.ProvidersImpl; import io.vertx.core.http.HttpServerResponse; /** * Provides CDI producers for objects that can be injected via @Context - * In quarkus-rest this works because @Context is considered an alias for @Inject + * In RESTEasy Reactive this works because @Context is considered an alias for @Inject * through the use of {@code AutoInjectAnnotationBuildItem} */ @Singleton @@ -21,4 +24,10 @@ public class QuarkusContextProducers { HttpServerResponse httpServerResponse() { return CurrentRequestManager.get().serverRequest().unwrap(HttpServerResponse.class); } + + @ApplicationScoped + @Produces + Providers providers() { + return new ProvidersImpl(ResteasyReactiveRecorder.getCurrentDeployment()); + } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/beanparam/BeanParamTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/beanparam/BeanParamTest.java new file mode 100644 index 0000000000000..47abd07a4f4c9 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/beanparam/BeanParamTest.java @@ -0,0 +1,252 @@ +package io.quarkus.rest.client.reactive.beanparam; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; + +import javax.ws.rs.CookieParam; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.jboss.resteasy.reactive.RestCookie; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.RestHeader; +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; +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 BeanParamTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest(); + + @TestHTTPResource + URI baseUri; + + @Test + void shouldPassPathParamFromBeanParam() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + assertThat(client.beanParamWithFields(new MyBeanParamWithFields())) + .isEqualTo("restPathDefault/restPathOverridden/pathParam" + + "/restHeaderDefault/restHeaderOverridden/headerParam" + + "/restFormDefault/restFormOverridden/formParam" + + "/restCookieDefault/restCookieOverridden/cookieParam" + + "/restQueryDefault/restQueryOverridden/queryParam"); + assertThat(client.regularParameters( + "restPathDefault", "restPathOverridden", "pathParam", + "restHeaderDefault", "restHeaderOverridden", "headerParam", + "restCookieDefault", "restCookieOverridden", "cookieParam", + "restFormDefault", "restFormOverridden", "formParam", + "restQueryDefault", "restQueryOverridden", "queryParam")) + .isEqualTo("restPathDefault/restPathOverridden/pathParam" + + "/restHeaderDefault/restHeaderOverridden/headerParam" + + "/restFormDefault/restFormOverridden/formParam" + + "/restCookieDefault/restCookieOverridden/cookieParam" + + "/restQueryDefault/restQueryOverridden/queryParam"); + assertThat(client.beanParamWithProperties(new MyBeanParamWithProperties())).isEqualTo("null/null/pathParam" + + "/null/null/headerParam" + + "/null/null/formParam" + + "/null/null/cookieParam" + + "/null/null/queryParam"); + } + + public interface Client { + @Path("/{restPathDefault}/{restPath_Overridden}/{pathParam}") + @POST + String beanParamWithFields(MyBeanParamWithFields beanParam); + + @Path("/{restPathDefault}/{restPath_Overridden}/{pathParam}") + @POST + String regularParameters(@RestPath String restPathDefault, + @RestPath("restPath_Overridden") String restPathOverridden, + @PathParam("pathParam") String pathParam, + + @RestHeader String restHeaderDefault, + @RestHeader("restHeader_Overridden") String restHeaderOverridden, + @HeaderParam("headerParam") String headerParam, + + @RestCookie String restCookieDefault, + @RestCookie("restCookie_Overridden") String restCookieOverridden, + @CookieParam("cookieParam") String cookieParam, + + @RestForm String restFormDefault, + @RestForm("restForm_Overridden") String restFormOverridden, + @FormParam("formParam") String formParam, + + @RestQuery String restQueryDefault, + @RestQuery("restQuery_Overridden") String restQueryOverridden, + @QueryParam("queryParam") String queryParam); + + @Path("/{pathParam}") + @POST + String beanParamWithProperties(MyBeanParamWithProperties beanParam); + } + + public static class MyBeanParamWithFields { + @RestPath + private String restPathDefault = "restPathDefault"; + @RestPath("restPath_Overridden") + private String restPathOverridden = "restPathOverridden"; + @PathParam("pathParam") + private String pathParam = "pathParam"; + + @RestHeader + private String restHeaderDefault = "restHeaderDefault"; + @RestHeader("restHeader_Overridden") + private String restHeaderOverridden = "restHeaderOverridden"; + @HeaderParam("headerParam") + private String headerParam = "headerParam"; + + @RestCookie + private String restCookieDefault = "restCookieDefault"; + @RestCookie("restCookie_Overridden") + private String restCookieOverridden = "restCookieOverridden"; + @CookieParam("cookieParam") + private String cookieParam = "cookieParam"; + + @RestForm + private String restFormDefault = "restFormDefault"; + @RestForm("restForm_Overridden") + private String restFormOverridden = "restFormOverridden"; + @FormParam("formParam") + private String formParam = "formParam"; + + @RestQuery + private String restQueryDefault = "restQueryDefault"; + @RestQuery("restQuery_Overridden") + private String restQueryOverridden = "restQueryOverridden"; + @QueryParam("queryParam") + private String queryParam = "queryParam"; + + // FIXME: Matrix not supported + } + + public static class MyBeanParamWithProperties { + // FIXME: not allowed yet + // @RestPath + // public String getRestPathDefault(){ + // return "restPathDefault"; + // } + // @RestPath("restPath_Overridden") + // public String getRestPathOverridden(){ + // return "restPathOverridden"; + // } + @PathParam("pathParam") + public String getPathParam() { + return "pathParam"; + } + + // @RestHeader + // public String getRestHeaderDefault(){ + // return "restHeaderDefault"; + // } + // @RestHeader("restHeader_Overridden") + // public String getRestHeaderOverridden(){ + // return "restHeaderOverridden"; + // } + @HeaderParam("headerParam") + public String getHeaderParam() { + return "headerParam"; + } + + // @RestCookie + // public String getRestCookieDefault(){ + // return "restCookieDefault"; + // } + // @RestCookie("restCookie_Overridden") + // public String getRestCookieOverridden(){ + // return "restCookieOverridden"; + // } + @CookieParam("cookieParam") + public String getCookieParam() { + return "cookieParam"; + } + + // @RestForm + // public String getRestFormDefault(){ + // return "restFormDefault"; + // } + // @RestForm("restForm_Overridden") + // public String getRestFormOverridden(){ + // return "restFormOverridden"; + // } + @FormParam("formParam") + public String getFormParam() { + return "formParam"; + } + + // @RestQuery + // public String getRestQueryDefault(){ + // return "restQueryDefault"; + // } + // @RestQuery("restQuery_Overridden") + // public String getRestQueryOverridden(){ + // return "restQueryOverridden"; + // } + @QueryParam("queryParam") + public String getQueryParam() { + return "queryParam"; + } + + // FIXME: Matrix not supported + } + + @Path("/") + public static class Resource { + @Path("/{restPathDefault}/{restPath_Overridden}/{pathParam}") + @POST + public String beanParamWithFields(@RestPath String restPathDefault, + @RestPath String restPath_Overridden, + @RestPath String pathParam, + @RestHeader String restHeaderDefault, + @RestHeader("restHeader_Overridden") String restHeader_Overridden, + @RestHeader("headerParam") String headerParam, + @RestForm String restFormDefault, + @RestForm String restForm_Overridden, + @RestForm String formParam, + @RestCookie String restCookieDefault, + @RestCookie String restCookie_Overridden, + @RestCookie String cookieParam, + @RestQuery String restQueryDefault, + @RestQuery String restQuery_Overridden, + @RestQuery String queryParam) { + return restPathDefault + "/" + restPath_Overridden + "/" + pathParam + + "/" + restHeaderDefault + "/" + restHeader_Overridden + "/" + headerParam + + "/" + restFormDefault + "/" + restForm_Overridden + "/" + formParam + + "/" + restCookieDefault + "/" + restCookie_Overridden + "/" + cookieParam + + "/" + restQueryDefault + "/" + restQuery_Overridden + "/" + queryParam; + } + + @Path("/{pathParam}") + @POST + public String beanParamWithProperties(@RestPath String restPathDefault, + @RestPath String restPath_Overridden, + @RestPath String pathParam, + @RestHeader String restHeaderDefault, + @RestHeader("restHeader_Overridden") String restHeader_Overridden, + @RestHeader("headerParam") String headerParam, + @RestForm String restFormDefault, + @RestForm String restForm_Overridden, + @RestForm String formParam, + @RestCookie String restCookieDefault, + @RestCookie String restCookie_Overridden, + @RestCookie String cookieParam, + @RestQuery String restQueryDefault, + @RestQuery String restQuery_Overridden, + @RestQuery String queryParam) { + return restPathDefault + "/" + restPath_Overridden + "/" + pathParam + + "/" + restHeaderDefault + "/" + restHeader_Overridden + "/" + headerParam + + "/" + restFormDefault + "/" + restForm_Overridden + "/" + formParam + + "/" + restCookieDefault + "/" + restCookie_Overridden + "/" + cookieParam + + "/" + restQueryDefault + "/" + restQuery_Overridden + "/" + queryParam; + } + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/HeaderTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/HeaderTest.java index af444efd2556b..22d902c8e8406 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/HeaderTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/HeaderTest.java @@ -8,8 +8,11 @@ import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.jboss.resteasy.reactive.RestHeader; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -27,13 +30,14 @@ public class HeaderTest { @Test void testHeadersWithSubresource() { Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); - assertThat(client.cookieSub("bar", "bar2").send("bar3", "bar4")).isEqualTo("bar:bar2:bar3:bar4"); + assertThat(client.cookieSub("bar", "bar2").send("bar3", "bar4", "dummy")) + .isEqualTo("bar:bar2:bar3:bar4:X-My-Header/dummy"); } @Test void testNullHeaders() { Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); - assertThat(client.cookieSub("bar", null).send(null, "bar4")).isEqualTo("bar:null:null:bar4"); + assertThat(client.cookieSub("bar", null).send(null, "bar4", "dummy")).isEqualTo("bar:null:null:bar4:X-My-Header/dummy"); } @Path("/") @@ -41,8 +45,11 @@ void testNullHeaders() { public static class Resource { @GET public String returnHeaders(@HeaderParam("foo") String header, @HeaderParam("foo2") String header2, - @HeaderParam("foo3") String header3, @HeaderParam("foo4") String header4) { - return header + ":" + header2 + ":" + header3 + ":" + header4; + @HeaderParam("foo3") String header3, @HeaderParam("foo4") String header4, @Context HttpHeaders headers) { + String myHeaderName = headers.getRequestHeaders().keySet().stream().filter(s -> s.equalsIgnoreCase("X-My-Header")) + .findFirst().orElse(""); + return header + ":" + header2 + ":" + header3 + ":" + header4 + ":" + myHeaderName + "/" + + headers.getHeaderString("X-My-Header"); } } @@ -55,7 +62,7 @@ public interface Client { public interface SubClient { @GET - String send(@HeaderParam("foo3") String cookie3, @HeaderParam("foo4") String cookie4); + String send(@HeaderParam("foo3") String cookie3, @HeaderParam("foo4") String cookie4, @RestHeader String xMyHeader); } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/ComputedHeader.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/ComputedHeader.java new file mode 100644 index 0000000000000..3a796b6a0988d --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/ComputedHeader.java @@ -0,0 +1,9 @@ +package io.quarkus.rest.client.reactive.registerclientheaders; + +public final class ComputedHeader { + + public static String get() { + return "From " + ComputedHeader.class.getName(); + } + +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/MultipleHeadersBindingClient.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/MultipleHeadersBindingClient.java new file mode 100644 index 0000000000000..7a4e8012169dd --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/MultipleHeadersBindingClient.java @@ -0,0 +1,20 @@ +package io.quarkus.rest.client.reactive.registerclientheaders; + +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient +@RegisterClientHeaders(MyHeadersFactory.class) +@ClientHeaderParam(name = "my-header", value = "constant-header-value") +@ClientHeaderParam(name = "computed-header", value = "{io.quarkus.rest.client.reactive.registerclientheaders.ComputedHeader.get}") +public interface MultipleHeadersBindingClient { + @GET + @Path("/describe-request") + @ClientHeaderParam(name = "header-from-properties", value = "${header.value}") + RequestData call(@HeaderParam("jaxrs-style-header") String headerValue); +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/MyHeadersFactory.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/MyHeadersFactory.java index 8572362e62d2e..79068e1a0695f 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/MyHeadersFactory.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/MyHeadersFactory.java @@ -19,8 +19,8 @@ public class MyHeadersFactory implements ClientHeadersFactory { public MultivaluedMap update(MultivaluedMap incomingHeaders, MultivaluedMap clientOutgoingHeaders) { assertNotNull(beanManager); - incomingHeaders.add("foo", "bar"); - return incomingHeaders; + clientOutgoingHeaders.add("foo", "bar"); + return clientOutgoingHeaders; } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/RegisterClientHeadersTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/RegisterClientHeadersTest.java index 670e5213d8f3c..7d9488644b613 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/RegisterClientHeadersTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/registerclientheaders/RegisterClientHeadersTest.java @@ -1,9 +1,13 @@ package io.quarkus.rest.client.reactive.registerclientheaders; import static io.quarkus.rest.client.reactive.RestClientTestUtil.setUrlForClass; +import static io.quarkus.rest.client.reactive.registerclientheaders.HeaderSettingClient.HEADER; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.List; +import java.util.Map; + import org.eclipse.microprofile.rest.client.inject.RestClient; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -26,8 +30,9 @@ public class RegisterClientHeadersTest { setUrlForClass(HeaderSettingClient.class) + setUrlForClass(HeaderPassingClient.class) + setUrlForClass(HeaderNoPassingClient.class) + - "org.eclipse.microprofile.rest.client.propagateHeaders=" - + HeaderSettingClient.HEADER), + setUrlForClass(MultipleHeadersBindingClient.class) + + "org.eclipse.microprofile.rest.client.propagateHeaders=" + HEADER + "\n" + + "header.value=from property file"), "application.properties"); }); @@ -37,6 +42,9 @@ public class RegisterClientHeadersTest { @RestClient HeaderSettingClient headerSettingClient; + @RestClient + MultipleHeadersBindingClient multipleHeadersBindingClient; + @Test public void shouldUseHeadersFactory() { assertEquals("ping:bar", client.echo("ping:")); @@ -47,14 +55,30 @@ public void shouldUseHeadersFactory() { public void shouldPassIncomingHeaders() { String headerValue = "my-header-value"; RequestData requestData = headerSettingClient.setHeaderValue(headerValue); - assertThat(requestData.getHeaders().get(HeaderSettingClient.HEADER).get(0)).isEqualTo(headerValue); + assertThat(requestData.getHeaders().get(HEADER).get(0)).isEqualTo(headerValue); } @Test public void shouldNotPassIncomingHeaders() { String headerValue = "my-header-value"; RequestData requestData = headerSettingClient.setHeaderValueNoPassing(headerValue); - assertThat(requestData.getHeaders().get(HeaderSettingClient.HEADER)).isNull(); + assertThat(requestData.getHeaders().get(HEADER)).isNull(); + } + + @Test + public void shouldSetHeadersFromMultipleBindings() { + String headerValue = "my-header-value"; + Map> headers = multipleHeadersBindingClient.call(headerValue).getHeaders(); + // Verify: @RegisterClientHeaders(MyHeadersFactory.class) + assertThat(headers.get("foo")).containsExactly("bar"); + // Verify: @ClientHeaderParam(name = "my-header", value = "constant-header-value") + assertThat(headers.get("my-header")).containsExactly("constant-header-value"); + // Verify: @ClientHeaderParam(name = "computed-header", value = "{...ComputedHeader.get}") + assertThat(headers.get("computed-header")).containsExactly("From " + ComputedHeader.class.getName()); + // Verify: @ClientHeaderParam(name = "header-from-properties", value = "${header.value}") + assertThat(headers.get("header-from-properties")).containsExactly("from property file"); + // Verify: @HeaderParam("jaxrs-style-header") + assertThat(headers.get("jaxrs-style-header")).containsExactly(headerValue); } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientRequestFilter.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientRequestFilter.java index 3a6c26f8405ed..89982a4c8caaa 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientRequestFilter.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/MicroProfileRestClientRequestFilter.java @@ -10,7 +10,6 @@ import javax.ws.rs.core.MultivaluedMap; import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory; -import org.eclipse.microprofile.rest.client.ext.DefaultClientHeadersFactoryImpl; import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext; import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestFilter; @@ -55,12 +54,9 @@ public void filter(ResteasyReactiveClientRequestContext requestContext) { } } - if (clientHeadersFactory instanceof DefaultClientHeadersFactoryImpl) { - // When using the default factory, pass the proposed outgoing headers onto the request context. - // Propagation with the default factory will then overwrite any values if required. - for (Map.Entry> headerEntry : headers.entrySet()) { - requestContext.getHeaders().put(headerEntry.getKey(), castToListOfObjects(headerEntry.getValue())); - } + // Propagation with the default factory will then overwrite any values if required. + for (Map.Entry> headerEntry : headers.entrySet()) { + requestContext.getHeaders().put(headerEntry.getKey(), castToListOfObjects(headerEntry.getValue())); } if (clientHeadersFactory != null) { @@ -82,10 +78,6 @@ public void filter(ResteasyReactiveClientRequestContext requestContext) { requestContext.getHeaders().put(headerEntry.getKey(), castToListOfObjects(headerEntry.getValue())); } } - } else { - for (Map.Entry> headerEntry : headers.entrySet()) { - requestContext.getHeaders().put(headerEntry.getKey(), castToListOfObjects(headerEntry.getValue())); - } } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java index 422f5317a2c55..60ca6948c5742 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java @@ -795,13 +795,17 @@ private static void fetchType(Type type, BeanDeployment beanDeployment) { private static void collectCallbacks(ClassInfo clazz, List callbacks, DotName annotation, IndexView index, Set knownMethods) { for (MethodInfo method : clazz.methods()) { - if (method.returnType().kind() == Kind.VOID && method.parameterTypes().isEmpty()) { - if (method.hasAnnotation(annotation) && !knownMethods.contains(method.name())) { + if (method.hasAnnotation(annotation) && !knownMethods.contains(method.name())) { + if (method.returnType().kind() == Kind.VOID && method.parameterTypes().isEmpty()) { callbacks.add(method); + } else { + // invalid signature - build a meaningful message. + throw new DefinitionException("Invalid signature for the method `" + method + "` from class `" + + method.declaringClass() + "`. Methods annotated with `" + annotation + "` must return" + + " `void` and cannot have parameters."); } - knownMethods.add(method.name()); } - + knownMethods.add(method.name()); } if (clazz.superName() != null) { ClassInfo superClass = getClassByName(index, clazz.superName()); @@ -866,7 +870,7 @@ private static Integer initAlternativePriority(AnnotationTarget target, Integer if (computedPriority != null) { if (alternativePriority != null) { LOGGER.infof( - "Computed priority [%s] overrides the priority [%s] declared via @Priority or @AlernativePriority", + "Computed priority [%s] overrides the priority [%s] declared via @Priority or @AlternativePriority", computedPriority, alternativePriority); } alternativePriority = computedPriority; diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/bean/lifecycle/inheritance/BeanLifecycleMethodsOverridenTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/bean/lifecycle/inheritance/BeanLifecycleMethodsOverridenTest.java index c549693afa73e..9bdbdd4283700 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/bean/lifecycle/inheritance/BeanLifecycleMethodsOverridenTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/bean/lifecycle/inheritance/BeanLifecycleMethodsOverridenTest.java @@ -19,7 +19,7 @@ public class BeanLifecycleMethodsOverridenTest { ArcTestContainer container = new ArcTestContainer(Bird.class, Eagle.class, Falcon.class); @Test - public void testOverridenMethodWithNoAnnotation() { + public void testOverriddenMethodWithNoAnnotation() { resetAll(); InstanceHandle falconInstanceHandle = Arc.container().instance(Falcon.class); falconInstanceHandle.get().ping(); @@ -31,7 +31,7 @@ public void testOverridenMethodWithNoAnnotation() { } @Test - public void testOverridenMethodWithLifecycleAnnotation() { + public void testOverriddenMethodWithLifecycleAnnotation() { resetAll(); InstanceHandle eagleInstanceHandle = Arc.container().instance(Eagle.class); eagleInstanceHandle.get().ping(); diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/validation/InvalidPostConstructTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/validation/InvalidPostConstructTest.java new file mode 100644 index 0000000000000..0994fc10c014d --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/validation/InvalidPostConstructTest.java @@ -0,0 +1,38 @@ +package io.quarkus.arc.test.validation; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.spi.DefinitionException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Unremovable; +import io.quarkus.arc.test.ArcTestContainer; +import io.smallrye.mutiny.Uni; + +public class InvalidPostConstructTest { + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder().beanClasses(InvalidBean.class).shouldFail().build(); + + @Test + public void testFailure() { + Throwable error = container.getFailure(); + assertNotNull(error); + assertTrue(error instanceof DefinitionException); + } + + @ApplicationScoped + @Unremovable + public static class InvalidBean { + + @PostConstruct + public Uni invalid() { + return Uni.createFrom().nullItem(); + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/validation/InvalidPostConstructWithParametersTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/validation/InvalidPostConstructWithParametersTest.java new file mode 100644 index 0000000000000..cab640a82436a --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/validation/InvalidPostConstructWithParametersTest.java @@ -0,0 +1,41 @@ +package io.quarkus.arc.test.validation; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.spi.DefinitionException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Unremovable; +import io.quarkus.arc.test.ArcTestContainer; + +public class InvalidPostConstructWithParametersTest { + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder().beanClasses(InvalidBean.class).shouldFail().build(); + + @Test + public void testFailure() { + Throwable error = container.getFailure(); + assertNotNull(error); + assertTrue(error instanceof DefinitionException); + Assertions.assertTrue(error.getMessage().contains("invalid(java.lang.String ignored)")); + Assertions.assertTrue(error.getMessage().contains("$InvalidBean")); + Assertions.assertTrue(error.getMessage().contains("PostConstruct")); + } + + @ApplicationScoped + @Unremovable + public static class InvalidBean { + + @PostConstruct + public void invalid(String ignored) { + + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/validation/InvalidPreDestroyTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/validation/InvalidPreDestroyTest.java new file mode 100644 index 0000000000000..26e2b7a46fcb0 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/validation/InvalidPreDestroyTest.java @@ -0,0 +1,42 @@ +package io.quarkus.arc.test.validation; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.spi.DefinitionException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Unremovable; +import io.quarkus.arc.test.ArcTestContainer; +import io.smallrye.mutiny.Multi; + +public class InvalidPreDestroyTest { + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder().beanClasses(InvalidBean.class).shouldFail().build(); + + @Test + public void testFailure() { + Throwable error = container.getFailure(); + assertNotNull(error); + assertTrue(error instanceof DefinitionException); + Assertions.assertTrue(error.getMessage().contains("invalid()")); + Assertions.assertTrue(error.getMessage().contains("$InvalidBean")); + Assertions.assertTrue(error.getMessage().contains("PreDestroy")); + } + + @ApplicationScoped + @Unremovable + public static class InvalidBean { + + @PreDestroy + public Multi invalid() { + return null; + } + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java index a9ae080e0da6c..82b30beec69df 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java @@ -20,37 +20,131 @@ static PathTree ofDirectoryOrFile(Path p) { } } + /** + * Creates a new {@link PathTree} for a given existing path which is expected to be + * either a directory or a ZIP-based archive. + * + * @param p path to a directory or an archive + * @return an instance of {@link PathTree} for a given existing directory or an archive + */ static PathTree ofDirectoryOrArchive(Path p) { + return ofDirectoryOrArchive(p, null); + } + + /** + * Creates a new {@link PathTree} for a given existing path which is expected to be + * either a directory or a ZIP-based archive applying a provided {@link PathFilter} + * unless it is {@code null}. + * + * @param p path to a directory or an archive + * @param filter path filter to apply, could be {@code null} + * @return an instance of {@link PathTree} for a given existing directory or an archive + */ + static PathTree ofDirectoryOrArchive(Path p, PathFilter filter) { try { final BasicFileAttributes fileAttributes = Files.readAttributes(p, BasicFileAttributes.class); - return fileAttributes.isDirectory() ? new DirectoryPathTree(p) : new ArchivePathTree(p); + return fileAttributes.isDirectory() ? new DirectoryPathTree(p, filter) : new ArchivePathTree(p, filter); } catch (IOException e) { throw new IllegalArgumentException(p + " does not exist", e); } } + /** + * Creates a new {@link PathTree} for an existing path that is expected to be + * a ZIP-based archive. + * + * @param archive path to an archive + * @return an instance of {@link PathTree} for a given archive + */ static PathTree ofArchive(Path archive) { + return ofArchive(archive, null); + } + + /** + * Creates a new {@link PathTree} for an existing path that is expected to be + * a ZIP-based archive applying a provided {@link PathFilter} unless it is {@code null}. + * + * @param archive path to an archive + * @param filter path filter to apply, could be {@code null} + * @return an instance of {@link PathTree} for a given archive + */ + static PathTree ofArchive(Path archive, PathFilter filter) { if (!Files.exists(archive)) { throw new IllegalArgumentException(archive + " does not exist"); } - return new ArchivePathTree(archive); + return new ArchivePathTree(archive, filter); } + /** + * The roots of the path tree. + * + * @return roots of the path tree + */ Collection getRoots(); + /** + * Checks whether the tree is empty + * + * @return true, if the tree is empty, otherwise - false + */ default boolean isEmpty() { return getRoots().isEmpty(); } + /** + * If {@code META-INF/MANIFEST.MF} found, reads it and returns an instance of {@link java.util.jar.Manifest}, + * otherwise returns null. + * + * @return parsed {@code META-INF/MANIFEST.MF} if it's found, otherwise {@code null} + */ Manifest getManifest(); + /** + * Walks the tree. + * + * @param visitor path visitor + */ void walk(PathVisitor visitor); + /** + * Applies a function to a given path relative to the root of the tree. + * If the path isn't found in the tree, the {@link PathVisit} argument + * passed to the function will be {@code null}. + * + * @param resulting type + * @param relativePath relative path to process + * @param func processing function + * @return result of the function + */ T apply(String relativePath, Function func); + /** + * Consumes a given path relative to the root of the tree. + * If the path isn't found in the tree, the {@link PathVisit} argument + * passed to the consumer will be {@code null}. + * + * @param relativePath relative path to consume + * @param consumer path consumer + */ void accept(String relativePath, Consumer consumer); + /** + * Checks whether the tree contains a relative path. + * + * @param relativePath path relative to the root of the tree + * @return true, in case the tree contains the path, otherwise - false + */ boolean contains(String relativePath); + /** + * Returns an {@link OpenPathTree} for this tree, which is supposed to be + * closed at the end of processing. It is meant to be an optimization when + * processing multiple paths of path trees that represent archives. + * If a path tree does not represent an archive but a directory, for example, + * this method is expected to be a no-op, returning the original tree as an + * instance of {@link OpenPathTree}. + * + * @return an instance of {@link OpenPathTree} for this path tree + */ OpenPathTree open(); } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java index 5b182c0b2f654..cba6b34e0c08b 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java @@ -13,6 +13,7 @@ import java.security.ProtectionDomain; import java.security.cert.Certificate; import java.util.HashSet; +import java.util.Iterator; import java.util.Objects; import java.util.Set; import java.util.concurrent.locks.ReadWriteLock; @@ -182,6 +183,26 @@ public void close() throws IOException { } } + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append(getClass().getName()).append("["); + if (getDependencyKey() != null) { + sb.append(getDependencyKey().toGacString()).append(" "); + } + final Iterator i = pathTree.getRoots().iterator(); + if (i.hasNext()) { + sb.append(i.next()); + while (i.hasNext()) { + sb.append(",").append(i.next()); + } + } + sb.append(" runtime=").append(isRuntime()); + final Set resources = this.resources; + sb.append(" resources=").append(resources == null ? "null" : resources.size()); + return sb.append(']').toString(); + } + private class Resource implements ClassPathResource { private final String name; diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index d269b949db945..75321822a384e 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -361,7 +361,12 @@ public URL getResource(String unsanitisedName) { if (name.endsWith(".class") && !endsWithTrailingSlash) { ClassPathElement[] providers = state.loadableResources.get(name); if (providers != null) { - return providers[0].getResource(name).getUrl(); + final ClassPathResource resource = providers[0].getResource(name); + if (resource == null) { + throw new IllegalStateException(providers[0] + " from " + getName() + " (closed=" + this.isClosed() + + ") was expected to provide " + name + " but failed"); + } + return resource.getUrl(); } } else { for (ClassPathElement i : elements) { diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index 2b3a8f72bece4..a5515274a6908 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -64,7 +64,7 @@ 1.7.36 22.2.0 2.6.0 - 1.13.1 + 1.13.2 7.5.1 0.0.9 0.1.1 diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/CheckedFragment.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/CheckedFragment.java deleted file mode 100644 index 6469c65f5d249..0000000000000 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/CheckedFragment.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.quarkus.qute; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Denotes a method that represents a fragment of a type-safe template. - *

- * The name of the fragment is derived from the annotated method name. The part before the last occurence of a dollar sign - * {@code $} is - * the method name of the related type-safe template. The part after the last occurence of a dollar sign is the fragment - * identifier - the strategy defined by the relevant {@link CheckedTemplate#defaultName()} is used. - *

- * Parameters of the annotated method are validated. The required names and types are derived from the relevant fragment - * template. - * - *

- * @CheckedTemplate
- * class Templates {
- *
- *     // defines a type-safe template
- *     static native TemplateInstance items(List<Item> items);
- *
- *     // defines a fragment of Templates#items() with identifier "item"
- *     @CheckedFragment
- *     static native TemplateInstance items$item(Item item);
- * }
- * 
- * - * @see CheckedTemplate - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface CheckedFragment { - -} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/CheckedTemplate.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/CheckedTemplate.java index 9f3ee3ea25f8d..f71c1ee194732 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/CheckedTemplate.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/CheckedTemplate.java @@ -49,6 +49,33 @@ * } * } * + * + *

Type-safe Fragments

+ * + * By default, a native static method with the name that contains a dollar sign {@code $} denotes a method that + * represents a fragment of a type-safe template. It's possible to ignore the fragments and effectively disable this + * feature via {@link CheckedTemplate#ignoreFragments()}. + *

+ * The name of the fragment is derived from the annotated method name. The part before the last occurence of a dollar sign + * {@code $} is the method name of the related type-safe template. The part after the last occurence of a dollar sign is the + * fragment identifier - the strategy defined by the relevant {@link CheckedTemplate#defaultName()} is used. + *

+ * Parameters of the annotated method are validated. The required names and types are derived from the relevant fragment + * template. + * + *

+ * @CheckedTemplate
+ * class Templates {
+ *
+ *     // defines a type-safe template
+ *     static native TemplateInstance items(List<Item> items);
+ *
+ *     // defines a fragment of Templates#items() with identifier "item"
+ *     @CheckedFragment
+ *     static native TemplateInstance items$item(Item item);
+ * }
+ * 
+ * */ @Documented @Retention(RetentionPolicy.RUNTIME) @@ -113,4 +140,14 @@ */ String defaultName() default ELEMENT_NAME; + /** + * By default, a native static method with the name that contains a dollar sign {@code $} denotes a method that + * represents a fragment of a type-safe template. It's possible to ignore the fragments and effectively disable this + * feature. + * + * @return {@code true} if no method should be interpreted as a fragment, {@code false} otherwise + * @see Template#getFragment(String) + */ + boolean ignoreFragments() default false; + } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java index a1ca039c882da..a4438a030fae9 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java @@ -113,7 +113,11 @@ protected boolean ignoreParameterInit(String key, String value) { // {#include foo _isolated=true /} || key.equals(ISOLATED) // {#include foo _isolated /} - || value.equals(ISOLATED); + || value.equals(ISOLATED) + // {#include foo _ignoreFragments=true /} + || key.equals(IGNORE_FRAGMENTS) + // {#include foo _ignoreFragments /} + || value.equals(IGNORE_FRAGMENTS); } @Override @@ -136,6 +140,7 @@ protected IncludeSectionHelper newHelper(Supplier