From ecf96edfd6ee7b4508a56b6713c9e66d0a4f8e86 Mon Sep 17 00:00:00 2001 From: Laurent Broudoux Date: Fri, 5 Apr 2024 09:25:55 +0200 Subject: [PATCH 01/72] feat: Allow finer tuning of shared network usage by DevServices Signed-off-by: Laurent Broudoux --- .../DevServicesSharedNetworkBuildItem.java | 50 ++++++++++++++++++- .../devservices/GlobalDevServicesConfig.java | 6 +++ .../deployment/DB2DevServicesProcessor.java | 9 +++- .../MariaDBDevServicesProcessor.java | 9 +++- .../deployment/MSSQLDevServicesProcessor.java | 7 ++- .../deployment/MySQLDevServicesProcessor.java | 9 +++- .../OracleDevServicesProcessor.java | 9 +++- .../PostgresqlDevServicesProcessor.java | 9 +++- .../DevServicesElasticsearchProcessor.java | 4 +- .../deployment/DevServicesMongoProcessor.java | 4 +- .../client/DevServicesRedisProcessor.java | 4 +- .../DevServicesApicurioRegistryProcessor.java | 4 +- .../test/junit/IntegrationTestUtil.java | 4 +- 13 files changed, 110 insertions(+), 18 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesSharedNetworkBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesSharedNetworkBuildItem.java index fbce82559bd27..459d3b9605e50 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesSharedNetworkBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/DevServicesSharedNetworkBuildItem.java @@ -9,15 +9,43 @@ import io.quarkus.builder.BuildChainBuilder; import io.quarkus.builder.BuildStepBuilder; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; /** * A marker build item that indicates, if any instances are provided during the build, the containers started by DevServices - * will use a shared network. + * may use a shared network. * This is mainly useful in integration tests where the application container needs to be able * to communicate with the service containers. */ public final class DevServicesSharedNetworkBuildItem extends MultiBuildItem { + private final String source; + + /** Create a build item without identifying the creator source. */ + public DevServicesSharedNetworkBuildItem() { + this.source = UNKNOWN_SOURCE; + } + + /** + * Create a build item identifying the creator source. + * + * @param source The identifier of the creator + */ + public DevServicesSharedNetworkBuildItem(String source) { + this.source = source; + } + + /** The creator source of this build item. May be useful to decide whether a DevService should join a shared network. */ + public String getSource() { + return source; + } + + /* Property used by factory to retrieve the source of instanciation. */ + public static final String SOURCE_PROPERTY = "source"; + + /* Value of source field when instanciation origin is unknown. */ + public static final String UNKNOWN_SOURCE = "unknown"; + /** * Generates a {@code List> build chain builder} which creates a build step * producing the {@link DevServicesSharedNetworkBuildItem} build item. @@ -28,10 +56,28 @@ public static final class Factory implements Function, List< public List> apply(final Map props) { return Collections.singletonList((builder) -> { BuildStepBuilder stepBuilder = builder.addBuildStep((ctx) -> { - ctx.produce(new DevServicesSharedNetworkBuildItem()); + DevServicesSharedNetworkBuildItem buildItem; + if (props != null && props.containsKey(SOURCE_PROPERTY)) { + buildItem = new DevServicesSharedNetworkBuildItem(props.get(SOURCE_PROPERTY).toString()); + } else { + buildItem = new DevServicesSharedNetworkBuildItem(); + } + ctx.produce(buildItem); }); stepBuilder.produces(DevServicesSharedNetworkBuildItem.class).build(); }); } } + + /** + * Helper method for DevServices processors that tells if joining the shared network is required. + * Joining this network may be required if explicitily asked by user properties or if running a containerized + * application during integration tests. + */ + public static boolean isSharedNetworkRequired(GlobalDevServicesConfig globalDevServicesConfig, + List devServicesSharedNetworkBuildItem) { + return globalDevServicesConfig.launchOnSharedNetwork || + (!devServicesSharedNetworkBuildItem.isEmpty() + && devServicesSharedNetworkBuildItem.get(0).getSource().equals("io.quarkus.test.junit")); + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java index c7a51d9fb9619..12102e2d70095 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/devservices/GlobalDevServicesConfig.java @@ -16,6 +16,12 @@ public class GlobalDevServicesConfig { @ConfigItem(defaultValue = "true") boolean enabled; + /** + * Global flag that can be used to force the attachmment of Dev Services to shared netxork. Default is false. + */ + @ConfigItem(defaultValue = "false") + public boolean launchOnSharedNetwork; + /** * The timeout for starting a container */ diff --git a/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java b/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java index 9940177775761..0eb630a700ee6 100644 --- a/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java +++ b/extensions/devservices/db2/src/main/java/io/quarkus/devservices/db2/deployment/DB2DevServicesProcessor.java @@ -20,6 +20,7 @@ import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -32,15 +33,19 @@ public class DB2DevServicesProcessor { @BuildStep DevServicesDatasourceProviderBuildItem setupDB2( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.DB2, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusDb2Container container = new QuarkusDb2Container(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), - !devServicesSharedNetworkBuildItem.isEmpty()); + useSharedNetwork); startupTimeout.ifPresent(container::withStartupTimeout); String effectiveUsername = containerConfig.getUsername().orElse(username.orElse(DEFAULT_DATABASE_USERNAME)); diff --git a/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java b/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java index eb859ae79f1fb..3ec8c1362a462 100644 --- a/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java +++ b/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java @@ -20,6 +20,7 @@ import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -35,16 +36,20 @@ public class MariaDBDevServicesProcessor { @BuildStep DevServicesDatasourceProviderBuildItem setupMariaDB( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.MARIADB, new DevServicesDatasourceProvider() { @SuppressWarnings("unchecked") @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusMariaDBContainer container = new QuarkusMariaDBContainer(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), - !devServicesSharedNetworkBuildItem.isEmpty()); + useSharedNetwork); startupTimeout.ifPresent(container::withStartupTimeout); String effectiveUsername = containerConfig.getUsername().orElse(username.orElse(DEFAULT_DATABASE_USERNAME)); diff --git a/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java b/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java index 8296bdfdd7f0b..40baa951bd618 100644 --- a/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java +++ b/extensions/devservices/mssql/src/main/java/io/quarkus/devservices/mssql/deployment/MSSQLDevServicesProcessor.java @@ -17,6 +17,7 @@ import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -34,13 +35,17 @@ public class MSSQLDevServicesProcessor { @BuildStep DevServicesDatasourceProviderBuildItem setupMSSQL( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.MSSQL, new DevServicesDatasourceProvider() { @SuppressWarnings("unchecked") @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusMSSQLServerContainer container = new QuarkusMSSQLServerContainer(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), !devServicesSharedNetworkBuildItem.isEmpty()); diff --git a/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java b/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java index b951e08147f4a..1b9341a7a8eac 100644 --- a/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java +++ b/extensions/devservices/mysql/src/main/java/io/quarkus/devservices/mysql/deployment/MySQLDevServicesProcessor.java @@ -20,6 +20,7 @@ import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -34,16 +35,20 @@ public class MySQLDevServicesProcessor { @BuildStep DevServicesDatasourceProviderBuildItem setupMysql( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.MYSQL, new DevServicesDatasourceProvider() { @SuppressWarnings("unchecked") @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusMySQLContainer container = new QuarkusMySQLContainer(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), - !devServicesSharedNetworkBuildItem.isEmpty()); + useSharedNetwork); startupTimeout.ifPresent(container::withStartupTimeout); String effectiveUsername = containerConfig.getUsername().orElse(username.orElse(DEFAULT_DATABASE_USERNAME)); diff --git a/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java b/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java index 34d6e32cd8398..58448a484745a 100644 --- a/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java +++ b/extensions/devservices/oracle/src/main/java/io/quarkus/devservices/oracle/deployment/OracleDevServicesProcessor.java @@ -20,6 +20,7 @@ import io.quarkus.datasource.deployment.spi.DevServicesDatasourceProviderBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -39,15 +40,19 @@ public class OracleDevServicesProcessor { @BuildStep DevServicesDatasourceProviderBuildItem setupOracle( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.ORACLE, new DevServicesDatasourceProvider() { @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusOracleServerContainer container = new QuarkusOracleServerContainer(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), - !devServicesSharedNetworkBuildItem.isEmpty()); + useSharedNetwork); startupTimeout.ifPresent(container::withStartupTimeout); String effectiveUsername = containerConfig.getUsername().orElse(username.orElse(DEFAULT_DATABASE_USERNAME)); diff --git a/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java b/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java index af09e7e9fc499..d02a3699d844f 100644 --- a/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java +++ b/extensions/devservices/postgresql/src/main/java/io/quarkus/devservices/postgresql/deployment/PostgresqlDevServicesProcessor.java @@ -25,6 +25,7 @@ import io.quarkus.deployment.builditem.ConsoleCommandBuildItem; import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerShutdownCloseable; import io.quarkus.devservices.common.Labels; @@ -42,16 +43,20 @@ ConsoleCommandBuildItem psqlCommand(DevServicesLauncherConfigResultBuildItem dev @BuildStep DevServicesDatasourceProviderBuildItem setupPostgres( - List devServicesSharedNetworkBuildItem) { + List devServicesSharedNetworkBuildItem, + GlobalDevServicesConfig globalDevServicesConfig) { return new DevServicesDatasourceProviderBuildItem(DatabaseKind.POSTGRESQL, new DevServicesDatasourceProvider() { @SuppressWarnings("unchecked") @Override public RunningDevServicesDatasource startDatabase(Optional username, Optional password, String datasourceName, DevServicesDatasourceContainerConfig containerConfig, LaunchMode launchMode, Optional startupTimeout) { + + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(globalDevServicesConfig, + devServicesSharedNetworkBuildItem); QuarkusPostgreSQLContainer container = new QuarkusPostgreSQLContainer(containerConfig.getImageName(), containerConfig.getFixedExposedPort(), - !devServicesSharedNetworkBuildItem.isEmpty()); + useSharedNetwork); startupTimeout.ifPresent(container::withStartupTimeout); String effectiveUsername = containerConfig.getUsername().orElse(username.orElse(DEFAULT_DATABASE_USERNAME)); diff --git a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java index eee0fdf9d98ac..769dd2f7205a8 100644 --- a/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java +++ b/extensions/elasticsearch-rest-client-common/deployment/src/main/java/io/quarkus/elasticsearch/restclient/common/deployment/DevServicesElasticsearchProcessor.java @@ -95,8 +95,10 @@ public DevServicesResultBuildItem startElasticsearchDevService( (launchMode.isTest() ? "(test) " : "") + "Dev Services for Elasticsearch starting:", consoleInstalledBuildItem, loggingSetupBuildItem); try { + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(devServicesConfig, + devServicesSharedNetworkBuildItem); devService = startElasticsearch(dockerStatusBuildItem, configuration, buildItemsConfig, launchMode, - !devServicesSharedNetworkBuildItem.isEmpty(), + useSharedNetwork, devServicesConfig.timeout); if (devService == null) { compressor.closeAndDumpCaptured(); diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java index 20fae0ff4aef7..9b35f3954a706 100644 --- a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java @@ -93,8 +93,10 @@ public List startMongo(List startRedisContainers(LaunchModeBuildItem try { for (Entry entry : currentDevServicesConfiguration.entrySet()) { String connectionName = entry.getKey(); + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(devServicesConfig, + devServicesSharedNetworkBuildItem); RunningDevService devService = startContainer(dockerStatusBuildItem, connectionName, entry.getValue().devservices(), launchMode.getLaunchMode(), - !devServicesSharedNetworkBuildItem.isEmpty(), devServicesConfig.timeout); + useSharedNetwork, devServicesConfig.timeout); if (devService == null) { continue; } diff --git a/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/DevServicesApicurioRegistryProcessor.java b/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/DevServicesApicurioRegistryProcessor.java index 0ca85d928ad65..b80ab5eb86163 100644 --- a/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/DevServicesApicurioRegistryProcessor.java +++ b/extensions/schema-registry/devservice/deployment/src/main/java/io/quarkus/apicurio/registry/devservice/DevServicesApicurioRegistryProcessor.java @@ -79,8 +79,10 @@ public DevServicesResultBuildItem startApicurioRegistryDevService(LaunchModeBuil (launchMode.isTest() ? "(test) " : "") + "Apicurio Registry Dev Services Starting:", consoleInstalledBuildItem, loggingSetupBuildItem); try { + boolean useSharedNetwork = DevServicesSharedNetworkBuildItem.isSharedNetworkRequired(devServicesConfig, + devServicesSharedNetworkBuildItem); devService = startApicurioRegistry(dockerStatusBuildItem, configuration, launchMode, - !devServicesSharedNetworkBuildItem.isEmpty(), devServicesConfig.timeout); + useSharedNetwork, devServicesConfig.timeout); compressor.close(); } catch (Throwable t) { compressor.closeAndDumpCaptured(); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index 81d0fd3b41bf1..93a3d3f9568d1 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -289,7 +289,9 @@ static ArtifactLauncher.InitContext.DevServicesLaunchResult handleDevServices(Ex // when the application is going to be launched as a docker container, we need to make containers started by DevServices // use a shared network that the application container can then use as well augmentAction = curatedApplication.createAugmentor( - "io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem$Factory", Collections.emptyMap()); + "io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem$Factory", + Map.of(io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem.SOURCE_PROPERTY, + "io.quarkus.test.junit")); } else { augmentAction = curatedApplication.createAugmentor(); } From eb6b1ffa81af8ab20da9c33f34455e6eaf5c1fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Mon, 8 Apr 2024 15:17:29 +0200 Subject: [PATCH 02/72] Make a custom ContextManagerProvider with a single manager Because Quarkus doesn't care about one-per CL and Franz said this is a bottleneck for some reason --- .../QuarkusContextManagerProvider.java | 42 +++++++++++++++++++ .../SmallRyeContextPropagationRecorder.java | 3 +- 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/QuarkusContextManagerProvider.java diff --git a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/QuarkusContextManagerProvider.java b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/QuarkusContextManagerProvider.java new file mode 100644 index 0000000000000..9f334e8c3fc7c --- /dev/null +++ b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/QuarkusContextManagerProvider.java @@ -0,0 +1,42 @@ +package io.quarkus.smallrye.context.runtime; + +import org.eclipse.microprofile.context.spi.ContextManager; + +import io.smallrye.context.SmallRyeContextManager; +import io.smallrye.context.SmallRyeContextManagerProvider; + +/** + * Quarkus doesn't need one manager per CL, we only have the one + */ +public class QuarkusContextManagerProvider extends SmallRyeContextManagerProvider { + + private SmallRyeContextManager contextManager; + + @Override + public SmallRyeContextManager getContextManager(ClassLoader classLoader) { + return contextManager; + } + + @Override + public SmallRyeContextManager getContextManager() { + return contextManager; + } + + @Override + public ContextManager findContextManager(ClassLoader classLoader) { + return contextManager; + } + + @Override + public void registerContextManager(ContextManager manager, ClassLoader classLoader) { + if (manager instanceof SmallRyeContextManager == false) { + throw new IllegalArgumentException("Only instances of SmallRyeContextManager are supported: " + manager); + } + contextManager = (SmallRyeContextManager) manager; + } + + @Override + public void releaseContextManager(ContextManager manager) { + contextManager = null; + } +} diff --git a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java index b1a40ce4a00c7..93a2d0d3cbc37 100644 --- a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java +++ b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java @@ -14,7 +14,6 @@ import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.smallrye.context.SmallRyeContextManager; -import io.smallrye.context.SmallRyeContextManagerProvider; import io.smallrye.context.SmallRyeManagedExecutor; import io.smallrye.context.SmallRyeThreadContext; @@ -31,7 +30,7 @@ public void configureStaticInit(List discoveredProviders, // build the manager at static init time // in the live-reload mode, the provider instance may be already set in the previous start if (ContextManagerProvider.INSTANCE.get() == null) { - ContextManagerProvider contextManagerProvider = new SmallRyeContextManagerProvider(); + ContextManagerProvider contextManagerProvider = new QuarkusContextManagerProvider(); ContextManagerProvider.register(contextManagerProvider); } From 31225572d7f7986273179d71ac19fd0642530b76 Mon Sep 17 00:00:00 2001 From: Andy Damevin Date: Mon, 8 Apr 2024 17:19:58 +0200 Subject: [PATCH 03/72] Introduce Quarkus for the Web documentation --- docs/src/main/asciidoc/http-reference.adoc | 31 ++ .../asciidoc/images/web-bundle-transition.png | Bin 0 -> 105564 bytes docs/src/main/asciidoc/qute.adoc | 51 ++-- docs/src/main/asciidoc/web.adoc | 273 ++++++++++++++++++ 4 files changed, 330 insertions(+), 25 deletions(-) create mode 100644 docs/src/main/asciidoc/images/web-bundle-transition.png create mode 100644 docs/src/main/asciidoc/web.adoc diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 6f90ed502831c..e23ac5ec0b3cd 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -23,6 +23,8 @@ In its absence, RESTEasy operates directly on Vert.x without involving Servlets. == Serving Static Resources +If you are looking to use Quarkus for your Web Application, you might want to check the xref:web.adoc[Quarkus for the Web] guide. + === From the application jar To serve static resources from the application jar, you must place them in the `META-INF/resources` directory of your application. This location @@ -30,6 +32,35 @@ was chosen as it is the standard location for resources in `jar` files as define Quarkus can be used without Servlet, following this convention allows existing code that places its resources in this location to function correctly. +[[from-mvnpm]] +=== From MVNPM + +If you are using https://mvnpm.org/[mvnpm.org], like the following JQuery one: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + org.mvnpm + jquery + 3.7.1 + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("org.mvnpm:jquery:3.7.1") +---- + +You can use it in your html like this: +[source,html] +---- + +---- + + +[[from-webjars]] === From WebJars If you are using webjars, like the following JQuery one: diff --git a/docs/src/main/asciidoc/images/web-bundle-transition.png b/docs/src/main/asciidoc/images/web-bundle-transition.png new file mode 100644 index 0000000000000000000000000000000000000000..def6da4167c26b3661ee99e0e55c5525f2d1a2fe GIT binary patch literal 105564 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⪙N4h0 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 m&7D$on+NDrHRE=}j - io.quarkus - quarkus-rest-qute + io.quarkiverse.qute.web + quarkus-qute-web ---- + [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- -implementation("io.quarkus:quarkus-rest-qute") +implementation("io.quarkiverse.qute.web:quarkus-qute-web") ---- -* or `quarkus-resteasy-qute` if you are using RESTEasy Classic: -+ -[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] -.pom.xml ----- - - io.quarkus - quarkus-resteasy-qute - ----- -+ -[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] -.build.gradle +All files located in the `src/main/resources/templates` directory and its subdirectories are registered as templates. Templates are validated during startup and watched for changes in the development mode. + +Now, let's start with a Hello World html template: + +.src/main/resources/templates/pub/hello.html +[source] ---- -implementation("io.quarkus:quarkus-resteasy-qute") +

Hello {http:param('name', 'Quarkus')}!

<1> ---- +<1> `{http:param('name', 'Quarkus')}` is an expression that is evaluated when the template is rendered (Quarkus is the default value). -We'll start with a very simple template: +NOTE: Templates located in the `pub` directory are served via HTTP. Automatically, no controllers needed. For example, the template src/main/resource/templates/pub/foo.html will be served from the paths /foo and /foo.html by default. + +If your application is running, you can open your browser and hit: http://localhost:8080/hello?name=Martin + +For more information about Qute Web, see the https://docs.quarkiverse.io/quarkus-qute-web/dev/index.html[Qute Web guide]. + +[[hello-qute-rest]] +== Hello World with Jakarta REST + +If you want to use Qute in your Jakarta REST application, you still need to add the Qute Web extension first (see <>) and make sure you have the Quarkus REST (formerly RESTEasy Reactive) extension. + +Here is a very simple text template: .hello.txt [source] @@ -73,8 +76,6 @@ Hello {name}! <1> ---- <1> `{name}` is a value expression that is evaluated when the template is rendered. -NOTE: By default, all files located in the `src/main/resources/templates` directory and its subdirectories are registered as templates. Templates are validated during startup and watched for changes in the development mode. - Now let's inject the "compiled" template in the resource class. .HelloResource.java diff --git a/docs/src/main/asciidoc/web.adoc b/docs/src/main/asciidoc/web.adoc new file mode 100644 index 0000000000000..2402552005e2d --- /dev/null +++ b/docs/src/main/asciidoc/web.adoc @@ -0,0 +1,273 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Quarkus for the Web +include::_attributes.adoc[] +:categories: web +:summary: Learn more about creating all kinds of Web applications with Quarkus. +:numbered: +:sectnums: +:sectnumlevels: 3 +:topics: http,web,renarde,full-stack,qute,quinoa,web-bundler,mvc,ssr,nodejs,npm,javascript,css,jsf +:extensions: o.quarkiverse.qute.web:quarkus-qute-web,io.quarkiverse.renarde:quarkus-renarde,io.quarkiverse.web-bundler:quarkus-web-bundler,io.quarkiverse.quinoa:quarkus-quinoa + +Quarkus provides different extensions to create web applications, this document aims to provide directions on which extension to use for different use cases. + +== The basics + +=== Serving Static Resources + +Let's assume you have a Quarkus backend, and you want to serve static files. This is the most basic case, it is supported out of the box with all our Vert.x based extensions, you must place them in the `META-INF/resources` directory of your application. + +You can find more information in xref:http-reference#serving-static-resources[The HTTP Reference]. + +=== Serving Scripts, Styles, and web libraries + +However, if you want to insert scripts, styles, and libraries in your web pages, you have 3 options: + +a. Consume libraries from public CDNs such as cdnjs, unpkg, jsDelivr and more, or copy them to your `META-INF/resources` directory. +b. Use runtime web dependencies such as mvnpm.org or webjars, when added to your pom.xml or build.gradle they can be directly xref:http-reference#from-mvnpm[accessed from your web pages]. +c. Package your scripts (js, ts), styles (css, scss), and web dependencies together using a bundler (see xref:#bundling[below]). + +NOTE: *We recommend using a bundler for production* as it offers better control, consistency, security, and performance. The good news is that Quarkus makes it really easy and fast with the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Quarkus Web Bundler extension]. + +[[bundling]] +=== Bundling Scripts, Styles, and Libraries + +There are two ways to bundle your web assets: + +a. Using https://docs.quarkiverse.io/quarkus-web-bundler/dev/[the Quarkus Web Bundler extension], which is the recommended way. Without any configuration, it puts everything together in an instant, and follows good practices such as dead-code elimination, minification, caching, and more. +b. Using a custom bundler such as Webpack, Parcel, Rollup, etc. This can be easily integrated with Quarkus using the https://quarkiverse.github.io/quarkiverse-docs/quarkus-quinoa/dev/[Quarkus Quinoa extension]. + +image::web-bundle-transition.png[Web Bundle Transition] + +== Server-side rendering (SSR) + +For templating and server-side rendering with Quarkus, there are different engines available such as xref:qute.adoc[Qute] or https://docs.quarkiverse.io/quarkus-freemarker/dev/[Freemarker] and others. + +=== Qute Web + +Qute is designed specifically to meet the Quarkus needs, and help you deal with templates, snippets, and partials and render the data from your storage. It is inspired by the most famous template engines, it is fast, type-safe, works in native, and has a lot of nice features. + +To install Qute Web, follow xref:qute.adoc[the instructions]. + +Here is a simple example of a Qute template: + +.src/main/resources/templates/pub/index.html +[source,html] +---- + + + + + Qute Page + {#bundle /} <1> + + +

Hello {http:param('name', 'Quarkus')}

<2> +
    + {#for item in cdi:Product.items} <3> +
  • {item.name} {#if item.active}{item.price}{/if}
  • <4> + {/for} +
+ + +---- + +<1> With the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Web Bundler extension], this expression will be replaced by the bundled scripts and styles. +<2> You can directly use the HTTP parameters in your templates. +<3> This expression is validated. Try to change the expression to `cdi:Product.notHere` and the build should fail. +<4> If you install xref:ide-tooling.adoc[Quarkus IDEs plugins], you will have autocompletion, link to implementation and validation. + +=== Model-View-Controller (MVC) + +The MVC approach is also made very easy with Quarkus thanks to https://docs.quarkiverse.io/quarkus-renarde/dev/index.html[the Renarde extension], a Rails-like framework using Qute. + +Associated with the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Web Bundler extension], the road is open to build modern web applications for all you needs. Here is what a simple Renarde controller looks like: + +.src/main/java/rest/Todos.java +[source,java] +---- +package rest; + +[...] + +public class Todos extends Controller { + + @CheckedTemplate + static class Templates { + public static native TemplateInstance index(List todos); + } + + public TemplateInstance index() { + // list every todo + List todos = Todo.listAll(); + // render the index template + return Templates.index(todos); + } + + @POST + public void add(@NotBlank @RestForm String task) { + // check if there are validation issues + if(validationFailed()) { + // go back to the index page + index(); + } + // create a new Todo + Todo todo = new Todo(); + todo.task = task; + todo.persist(); + // send loving message + flash("message", "Task added"); + // redirect to index page + index(); + } + + @POST + public void delete(@RestPath Long id) { + // find the Todo + Todo todo = Todo.findById(id); + notFoundIfNull(todo); + // delete it + todo.delete(); + // send loving message + flash("message", "Task deleted"); + // redirect to index page + index(); + } + + @POST + public void done(@RestPath Long id) { + // find the Todo + Todo todo = Todo.findById(id); + notFoundIfNull(todo); + // switch its done state + todo.done = !todo.done; + if(todo.done) + todo.doneDate = new Date(); + // send loving message + flash("message", "Task updated"); + // redirect to index page + index(); + } +} +---- + +== Single Page Applications + +Quarkus provides very solid tools for creating or integrating Single Page Applications to Quarkus (React, Angular, Vue, …), here are 3 options: + +* https://quarkiverse.github.io/quarkiverse-docs/quarkus-quinoa/dev/[Quarkus Quinoa] bridges your npm-compatible web application and Quarkus for both dev and prod. No need to install Node.js or configure your framework, it will detect it and use sensible defaults. +* The https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Quarkus Web Bundler] is also a good approach, it is closer to the Java ecosystem and removes a lot of boilerplate and configuration, it is fast and efficient. For examples of such SPAs, see https://github.com/quarkusio/code.quarkus.io[code.quarkus.io] and https://github.com/mvnpm/mvnpm[mvnpm.org]. +* Your automation using the https://github.com/eirslett/frontend-maven-plugin[maven-frontend-plugin] or similar tools. + +== Full-stack microservices (Micro-frontends) + +Quarkus is an excellent choice for both full-stack web components and full-stack microservices aka Micro-frontends. By utilizing the Web Bundler or Quinoa, you can significantly reduce boilerplate code and manage multiple services efficiently without much configuration duplication. + +For example the https://github.com/quarkusio/search.quarkus.io[Guide search] on https://quarkus.io[quarkus.io] uses the Web Bundler to create a full-stack web-component. With Lit Element for the web-component and OpenSearch for the indexation it is a nice way to enhance the static web-site experience in a dynamic way. + +More content about this is coming soon... +// Blog article in prep: https://github.com/quarkusio/quarkusio.github.io/issues/1934 + +== Other ways + +We described Quarkus most common ways to create web applications but there are other options: + +* Vaadin Flow is a unique framework that lets you build web apps directly from Java code without writing HTML or JavaScript +* JavaServer Faces is a specification for building component-based web apps in Java. There are 3 extensions to help with JSF in Quarkus: MyFaces, OmniFaces, PrimeFaces. +// 👆 Blog article in prep https://github.com/quarkusio/quarkusio.github.io/issues/1935 +* Create xref:building-my-first-extension.adoc[a new extension] for your favorite web framework + +== Testing your web applications + +For testing web applications, https://docs.quarkiverse.io/quarkus-playwright/dev/[Quarkus Playwright] is very easy to use. You can create effective cross-browser end-to-end tests mimicking user interaction and making sure your web application is working as a whole. The big advantage is that it benefits from all dev-services and Quarkus mocking features. + +[source,java] +---- +@QuarkusTest +@WithPlaywright +public class WebApplicationTest { + + @InjectPlaywright + BrowserContext context; + + @TestHTTPResource("/") + URL index; + + @Test + public void testIndex() { + final Page page = context.newPage(); + Response response = page.navigate(index.toString()); + Assertions.assertEquals("OK", response.statusText()); + + page.waitForLoadState(); + + String title = page.title(); + Assertions.assertEquals("My Awesome App", title); + + // Make sure the web app is loaded and hits the backend + final ElementHandle quinoaEl = page.waitForSelector(".toast-body.received"); + String greeting = quinoaEl.innerText(); + Assertions.assertEquals("Hello from REST", greeting); + } +} +---- + +== Q&A + +=== Why is Quarkus a very good option for Web Applications compared to other frameworks? + +Quarkus is well known for its backend extensions ecosystem and developer experience, if you combine it with great extensions for frontend, then it is a perfect mix! All the testing and dev-mode features are now available for both frontend and backend. + +=== What are the advantages of SSR (Server Side Rendering) over SPA (Single Page App)? +Here are the benefits of performing rendering work on the server: + +*Data Retrieval:* Fetching data on the server, closer to the data source. This enhances performance by reducing the time needed to retrieve data for rendering and minimizes client requests. + +*Enhanced Security:* Storage of sensitive data and logic is happening on the server, such as tokens and API keys, without exposing them to client-side risks. + +*Caching Efficiency:* Server-side rendering allows for result caching, which can be reused across users and subsequent requests. This optimizes performance and lowers costs by reducing rendering and data fetching per request. + +*Improved Initial Page Load and First Contentful Paint (FCP):* Generating HTML on the server enables users to view the page immediately, eliminating the need to wait for client-side JavaScript to download, parse, and execute for rendering. + +*Search Engine Optimization (SEO) and Social Media Shareability:* The rendered HTML aids search engine indexing and social network previews, enhancing discoverability and shareability. + + +=== I am hesitating between Quinoa and the Web Bundler, how should I make my decision? + +You have to think that the bundled output is essentially the same with both solutions. Also, switching from one to the other is not a big deal, the choice is about the developer experience and finding the best fit for your team. + +Some guidelines: + +*Go for Quinoa:* + +* You have an existing frontend configured with a npm-compatible build tool, Quinoa is the most direct option. +* You have a dedicated frontend team familiar with tools such as NPM, Yarn and other for building Single Page Apps. +* If you want to write Javascript unit tests (such as Jest, Jasmine, ..), it is not possible with the Web Bundler. However, you could publish a components library on NPM and consume it from the Web Bundler. +* If you use very specific bundling options or specific tools in your build process +* If you love package.json and configurations tweaking + +*Go for Web Bundler:* + +* For simple web applications, the Web Bundler is the easiest and fastest way to get started +* If you prefer to stay close to the Maven/Gradle ecosystem +(Node.js is not needed), it uses an extremely fast bundler for the web (esbuild) +* If you want to reduce boilerplate and configuration + + +=== How do I scale a Quarkus Web Application? + +Serving a few static pages and scripts from an existing Quarkus backend is not a big overhead, so scaling the full app is usually the simplest option. +You could also split it in two services: one for the backend and one for the frontend. However, in most cases, this approach wouldn’t yield substantial benefits compared to the initial method. + +If your application involves a substantial number of static resources, consider using a CDN. Both the Web-Bundler and Quinoa can be configured to work seamlessly with a CDN, providing improved performance and distribution of assets. + +It would be nice to have a blog article and benchmark about this topic, please open an issue if you are interested in writing it. + + + + + From ad06f24dbce939bae0c6d7344318cd2a93b96f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Tue, 9 Apr 2024 15:03:33 +0200 Subject: [PATCH 04/72] Make sure OIDC is set up after CP It uses Mutiny, which requires CP to be initialised --- .../io/quarkus/oidc/deployment/OidcBuildStep.java | 5 ++++- .../ContextPropagationInitializedBuildItem.java | 11 +++++++++++ .../SmallRyeContextPropagationProcessor.java | 3 +++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/ContextPropagationInitializedBuildItem.java diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index b4827b8306bf4..9bc2cae847574 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -65,6 +65,7 @@ import io.quarkus.oidc.runtime.TenantConfigBean; import io.quarkus.oidc.runtime.providers.AzureAccessTokenCustomizer; import io.quarkus.runtime.TlsConfig; +import io.quarkus.smallrye.context.deployment.ContextPropagationInitializedBuildItem; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.http.deployment.EagerSecurityInterceptorBindingBuildItem; import io.quarkus.vertx.http.deployment.HttpAuthMechanismAnnotationBuildItem; @@ -220,7 +221,9 @@ public SyntheticBeanBuildItem setup( OidcConfig config, OidcRecorder recorder, CoreVertxBuildItem vertxBuildItem, - TlsConfig tlsConfig) { + TlsConfig tlsConfig, + // this is required for setup ordering: we need CP set up + ContextPropagationInitializedBuildItem cpInitializedBuildItem) { return SyntheticBeanBuildItem.configure(TenantConfigBean.class).unremovable().types(TenantConfigBean.class) .supplier(recorder.setup(config, vertxBuildItem.getVertx(), tlsConfig)) .destroyer(TenantConfigBean.Destroyer.class) diff --git a/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/ContextPropagationInitializedBuildItem.java b/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/ContextPropagationInitializedBuildItem.java new file mode 100644 index 0000000000000..534494892a3a4 --- /dev/null +++ b/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/ContextPropagationInitializedBuildItem.java @@ -0,0 +1,11 @@ +package io.quarkus.smallrye.context.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Marker build item for build ordering. Signifies that CP is set up + * and ready for use. + */ +public final class ContextPropagationInitializedBuildItem extends SimpleBuildItem { + +} diff --git a/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/SmallRyeContextPropagationProcessor.java b/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/SmallRyeContextPropagationProcessor.java index f923fc6dcaf70..341986949891a 100644 --- a/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/SmallRyeContextPropagationProcessor.java +++ b/extensions/smallrye-context-propagation/deployment/src/main/java/io/quarkus/smallrye/context/deployment/SmallRyeContextPropagationProcessor.java @@ -96,6 +96,7 @@ void buildStatic(SmallRyeContextPropagationRecorder recorder, List cpInitializedBuildItem, BuildProducer feature, BuildProducer syntheticBeans) { feature.produce(new FeatureBuildItem(Feature.SMALLRYE_CONTEXT_PROPAGATION)); @@ -111,6 +112,8 @@ void build(SmallRyeContextPropagationRecorder recorder, .unremovable() .supplier(recorder.initializeManagedExecutor(executorBuildItem.getExecutorProxy())) .setRuntimeInit().done()); + + cpInitializedBuildItem.produce(new ContextPropagationInitializedBuildItem()); } // transform IPs for ManagedExecutor/ThreadContext that use config annotation and don't yet have @NamedInstance From b9cc3c2dc65a6f61641c83a940e13c116ce6cd0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Tue, 9 Apr 2024 15:03:56 +0200 Subject: [PATCH 05/72] Delay FT class init at runtime so CP is initialised --- .../deployment/SmallRyeFaultToleranceProcessor.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java index 345c8808dc85f..e15fb3ad077d0 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java @@ -50,6 +50,7 @@ import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.deployment.recording.RecorderContext; @@ -87,7 +88,8 @@ public void build(BuildProducer annotationsTran CombinedIndexBuildItem combinedIndexBuildItem, BuildProducer reflectiveClass, BuildProducer reflectiveMethod, - BuildProducer config) { + BuildProducer config, + BuildProducer runtimeInitializedClassBuildItems) { feature.produce(new FeatureBuildItem(Feature.SMALLRYE_FAULT_TOLERANCE)); @@ -95,6 +97,8 @@ public void build(BuildProducer annotationsTran ContextPropagationRequestContextControllerProvider.class.getName())); serviceProvider.produce(new ServiceProviderBuildItem(RunnableWrapper.class.getName(), ContextPropagationRunnableWrapper.class.getName())); + // make sure this is initialised at runtime, otherwise it will get a non-initialised ContextPropagationManager + runtimeInitializedClassBuildItems.produce(new RuntimeInitializedClassBuildItem(RunnableWrapper.class.getName())); IndexView index = combinedIndexBuildItem.getIndex(); From 08ba1d14ea05421c496ea33ee9359d8269f1cef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Wed, 10 Apr 2024 15:13:31 +0200 Subject: [PATCH 06/72] CP: set up a temporary no-context/no-executor ContextManagerProvider during boot Because some extensions (only known one is spring-cloud-config-client) use Vert.x via Mutiny before runtime init is done. Mutiny and CP are both not set up yet, due to missing executor, but I'm starting CP with zero contexts and an executor which throws with an explicit error message, which should be enough to boot until runtime is properly set up. --- .../SmallRyeContextPropagationRecorder.java | 105 +++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java index 93a2d0d3cbc37..4955a2bbf54b7 100644 --- a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java +++ b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java @@ -1,11 +1,18 @@ package io.quarkus.smallrye.context.runtime; +import java.util.Collection; import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.function.Supplier; import org.eclipse.microprofile.context.ManagedExecutor; import org.eclipse.microprofile.context.ThreadContext; +import org.eclipse.microprofile.context.spi.ContextManager.Builder; import org.eclipse.microprofile.context.spi.ContextManagerExtension; import org.eclipse.microprofile.context.spi.ContextManagerProvider; import org.eclipse.microprofile.context.spi.ThreadContextProvider; @@ -23,6 +30,92 @@ @Recorder public class SmallRyeContextPropagationRecorder { + private static final ExecutorService NOPE_EXECUTOR_SERVICE = new ExecutorService() { + + @Override + public void execute(Runnable command) { + nope(); + } + + @Override + public void shutdown() { + nope(); + } + + @Override + public List shutdownNow() { + nope(); + return null; + } + + @Override + public boolean isShutdown() { + nope(); + return false; + } + + @Override + public boolean isTerminated() { + nope(); + return false; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + nope(); + return false; + } + + @Override + public Future submit(Callable task) { + nope(); + return null; + } + + @Override + public Future submit(Runnable task, T result) { + nope(); + return null; + } + + @Override + public Future submit(Runnable task) { + nope(); + return null; + } + + @Override + public List> invokeAll(Collection> tasks) throws InterruptedException { + nope(); + return null; + } + + @Override + public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + nope(); + return null; + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + nope(); + return null; + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + nope(); + return null; + } + + private void nope() { + throw new RuntimeException( + "Trying to invoke ContextPropagation on a partially-configured ContextManager instance. You should wait until runtime init is done. You can do that by consuming the ContextPropagationBuildItem."); + } + }; private static SmallRyeContextManager.Builder builder; public void configureStaticInit(List discoveredProviders, @@ -39,6 +132,16 @@ public void configureStaticInit(List discoveredProviders, .getContextManagerBuilder(); builder.withThreadContextProviders(discoveredProviders.toArray(new ThreadContextProvider[0])); builder.withContextManagerExtensions(discoveredExtensions.toArray(new ContextManagerExtension[0])); + + // During boot, if anyone is using CP, they will get no propagation and an error if they try to use + // the executor. This is (so far) only for spring-cloud-config-client which uses Vert.x via Mutiny + // to load config before we're ready for runtime init + SmallRyeContextManager.Builder noContextBuilder = (SmallRyeContextManager.Builder) ContextManagerProvider.instance() + .getContextManagerBuilder(); + noContextBuilder.withThreadContextProviders(new ThreadContextProvider[0]); + noContextBuilder.withContextManagerExtensions(new ContextManagerExtension[0]); + noContextBuilder.withDefaultExecutorService(NOPE_EXECUTOR_SERVICE); + ContextManagerProvider.instance().registerContextManager(noContextBuilder.build(), null /* not used */); } public void configureRuntime(ExecutorService executorService, ShutdownContext shutdownContext) { @@ -58,7 +161,7 @@ public void run() { } }); //Avoid leaking the classloader: - this.builder = null; + SmallRyeContextPropagationRecorder.builder = null; } public Supplier initializeManagedExecutor(ExecutorService executorService) { From ef0592b95282e170890f815fe20f3ce9d3e6c5d7 Mon Sep 17 00:00:00 2001 From: Andy Damevin Date: Tue, 9 Apr 2024 14:59:30 +0200 Subject: [PATCH 07/72] Apply suggestions from code review Co-authored-by: Guillaume Smet --- docs/src/main/asciidoc/http-reference.adoc | 19 +++++----- docs/src/main/asciidoc/web.adoc | 41 +++++++++++----------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index e23ac5ec0b3cd..5201ebbc4df5e 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -21,9 +21,9 @@ For Servlet support, Quarkus employs a customized Undertow version that operates When Undertow is present, RESTEasy functions as a Servlet filter. In its absence, RESTEasy operates directly on Vert.x without involving Servlets. -== Serving Static Resources +== Serving static resources -If you are looking to use Quarkus for your Web Application, you might want to check the xref:web.adoc[Quarkus for the Web] guide. +If you are looking to use Quarkus for a web application, look at the xref:web.adoc[Quarkus for the Web] guide. === From the application jar @@ -33,30 +33,31 @@ Quarkus can be used without Servlet, following this convention allows existing c location to function correctly. [[from-mvnpm]] -=== From MVNPM +=== From mvnpm -If you are using https://mvnpm.org/[mvnpm.org], like the following JQuery one: +If you are using https://mvnpm.org/[mvnpm], as for the following JQuery dependency: [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- org.mvnpm - jquery - 3.7.1 + bootstrap + 5.3.3 + runtime ---- [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- -implementation("org.mvnpm:jquery:3.7.1") +runtimeOnly("org.mvnpm:bootstrap:5.3.3") ---- -You can use it in your html like this: +You can import it in your HTML like this: [source,html] ---- - + ---- diff --git a/docs/src/main/asciidoc/web.adoc b/docs/src/main/asciidoc/web.adoc index 2402552005e2d..c3ecc9b68ed2a 100644 --- a/docs/src/main/asciidoc/web.adoc +++ b/docs/src/main/asciidoc/web.adoc @@ -10,20 +10,20 @@ include::_attributes.adoc[] :numbered: :sectnums: :sectnumlevels: 3 -:topics: http,web,renarde,full-stack,qute,quinoa,web-bundler,mvc,ssr,nodejs,npm,javascript,css,jsf -:extensions: o.quarkiverse.qute.web:quarkus-qute-web,io.quarkiverse.renarde:quarkus-renarde,io.quarkiverse.web-bundler:quarkus-web-bundler,io.quarkiverse.quinoa:quarkus-quinoa +:topics: http,web,renarde,full-stack,qute,quinoa,web-bundler,mvc,ssr,nodejs,npm,javascript,css,jsf,faces +:extensions: io.quarkiverse.qute.web:quarkus-qute-web,io.quarkiverse.renarde:quarkus-renarde,io.quarkiverse.web-bundler:quarkus-web-bundler,io.quarkiverse.quinoa:quarkus-quinoa -Quarkus provides different extensions to create web applications, this document aims to provide directions on which extension to use for different use cases. +Quarkus provides several extensions to create web applications, this document aims to provide directions on which extension to use for different use cases. == The basics -=== Serving Static Resources +=== Serving static resources Let's assume you have a Quarkus backend, and you want to serve static files. This is the most basic case, it is supported out of the box with all our Vert.x based extensions, you must place them in the `META-INF/resources` directory of your application. -You can find more information in xref:http-reference#serving-static-resources[The HTTP Reference]. +You can find more information in the xref:http-reference#serving-static-resources[HTTP reference guide]. -=== Serving Scripts, Styles, and web libraries +=== Serving scripts, styles, and web libraries However, if you want to insert scripts, styles, and libraries in your web pages, you have 3 options: @@ -34,7 +34,7 @@ c. Package your scripts (js, ts), styles (css, scss), and web dependencies toget NOTE: *We recommend using a bundler for production* as it offers better control, consistency, security, and performance. The good news is that Quarkus makes it really easy and fast with the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Quarkus Web Bundler extension]. [[bundling]] -=== Bundling Scripts, Styles, and Libraries +=== Bundling scripts, styles, and libraries There are two ways to bundle your web assets: @@ -78,14 +78,14 @@ Here is a simple example of a Qute template: <1> With the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Web Bundler extension], this expression will be replaced by the bundled scripts and styles. <2> You can directly use the HTTP parameters in your templates. -<3> This expression is validated. Try to change the expression to `cdi:Product.notHere` and the build should fail. +<3> This expression is validated. Try to change the expression to `cdi:Product.notHere` and the build will fail. <4> If you install xref:ide-tooling.adoc[Quarkus IDEs plugins], you will have autocompletion, link to implementation and validation. === Model-View-Controller (MVC) The MVC approach is also made very easy with Quarkus thanks to https://docs.quarkiverse.io/quarkus-renarde/dev/index.html[the Renarde extension], a Rails-like framework using Qute. -Associated with the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Web Bundler extension], the road is open to build modern web applications for all you needs. Here is what a simple Renarde controller looks like: +Associated with the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Web Bundler extension], the road is open to build modern web applications for all your needs. Here is what a simple Renarde controller looks like: .src/main/java/rest/Todos.java [source,java] @@ -167,7 +167,7 @@ Quarkus provides very solid tools for creating or integrating Single Page Applic Quarkus is an excellent choice for both full-stack web components and full-stack microservices aka Micro-frontends. By utilizing the Web Bundler or Quinoa, you can significantly reduce boilerplate code and manage multiple services efficiently without much configuration duplication. -For example the https://github.com/quarkusio/search.quarkus.io[Guide search] on https://quarkus.io[quarkus.io] uses the Web Bundler to create a full-stack web-component. With Lit Element for the web-component and OpenSearch for the indexation it is a nice way to enhance the static web-site experience in a dynamic way. +For example the https://github.com/quarkusio/search.quarkus.io[Quarkus documentation search engine] on https://quarkus.io[quarkus.io] uses the Web Bundler to create a full-stack web-component. With Lit Element for the web-component and OpenSearch for the indexation it is a nice way to enhance the static web-site experience in a dynamic way. More content about this is coming soon... // Blog article in prep: https://github.com/quarkusio/quarkusio.github.io/issues/1934 @@ -176,10 +176,9 @@ More content about this is coming soon... We described Quarkus most common ways to create web applications but there are other options: -* Vaadin Flow is a unique framework that lets you build web apps directly from Java code without writing HTML or JavaScript -* JavaServer Faces is a specification for building component-based web apps in Java. There are 3 extensions to help with JSF in Quarkus: MyFaces, OmniFaces, PrimeFaces. -// 👆 Blog article in prep https://github.com/quarkusio/quarkusio.github.io/issues/1935 -* Create xref:building-my-first-extension.adoc[a new extension] for your favorite web framework +* https://quarkus.io/extensions/com.vaadin/vaadin-quarkus-extension/[Vaadin Flow extension], for this unique framework that lets you build web apps directly from Java code without writing HTML or JavaScript. +* JavaServer Faces (jsf) is a specification for building component-based web apps in Java. It available in Quarkus, the https://quarkus.io/extensions/org.apache.myfaces.core.extensions.quarkus/myfaces-quarkus/[MyFaces] extension is an implementation of Faces for Quarkus. https://quarkus.io/extensions/io.quarkiverse.primefaces/quarkus-primefaces/[PrimeFaces] is a Faces components suite, and https://quarkus.io/extensions/io.quarkiverse.omnifaces/quarkus-omnifaces/[OmniFaces], a utility library. More information is available in https://www.melloware.com/quarkus-faces-using-jsf-with-quarkus/[this blog post]. +* Create xref:building-my-first-extension.adoc[a new extension] for your favorite web framework. == Testing your web applications @@ -246,16 +245,16 @@ Some guidelines: * You have an existing frontend configured with a npm-compatible build tool, Quinoa is the most direct option. * You have a dedicated frontend team familiar with tools such as NPM, Yarn and other for building Single Page Apps. -* If you want to write Javascript unit tests (such as Jest, Jasmine, ..), it is not possible with the Web Bundler. However, you could publish a components library on NPM and consume it from the Web Bundler. -* If you use very specific bundling options or specific tools in your build process -* If you love package.json and configurations tweaking +* You want to write Javascript unit tests (such as Jest, Jasmine, ..), it is not possible with the Web Bundler. However, you could publish a components library on NPM and consume it from the Web Bundler. +* You use very specific bundling options or specific tools in your build process +* You love package.json and configurations tweaking *Go for Web Bundler:* * For simple web applications, the Web Bundler is the easiest and fastest way to get started -* If you prefer to stay close to the Maven/Gradle ecosystem +* You prefer to stay close to the Maven/Gradle ecosystem (Node.js is not needed), it uses an extremely fast bundler for the web (esbuild) -* If you want to reduce boilerplate and configuration +* You want to reduce boilerplate and configuration === How do I scale a Quarkus Web Application? @@ -263,9 +262,9 @@ Some guidelines: Serving a few static pages and scripts from an existing Quarkus backend is not a big overhead, so scaling the full app is usually the simplest option. You could also split it in two services: one for the backend and one for the frontend. However, in most cases, this approach wouldn’t yield substantial benefits compared to the initial method. -If your application involves a substantial number of static resources, consider using a CDN. Both the Web-Bundler and Quinoa can be configured to work seamlessly with a CDN, providing improved performance and distribution of assets. +If your application involves a substantial number of static resources, consider using a CDN. Both the Web Bundler and Quinoa can be configured to work seamlessly with a CDN, providing improved performance and distribution of assets. -It would be nice to have a blog article and benchmark about this topic, please open an issue if you are interested in writing it. +// It would be nice to have a blog article and benchmark about this topic. From d494fe11988f56800e600eede7eed5164c448c04 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 11 Apr 2024 11:16:49 +0300 Subject: [PATCH 08/72] Support using List for sending multiple form values in REST Client Fixes: #39996 --- .../JaxrsClientReactiveProcessor.java | 80 +++++++++++++------ .../rest/client/reactive/FormListTest.java | 51 ++++++++++++ 2 files changed, 108 insertions(+), 23 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormListTest.java diff --git a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 6ff286f6416f1..3816b652ac553 100644 --- a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -201,6 +201,8 @@ public class JaxrsClientReactiveProcessor { String.class, Object.class); private static final MethodDescriptor MULTIVALUED_MAP_ADD = MethodDescriptor.ofMethod(MultivaluedMap.class, "add", void.class, Object.class, Object.class); + private static final MethodDescriptor MULTIVALUED_MAP_ADD_ALL = MethodDescriptor.ofMethod(MultivaluedMap.class, "addAll", + void.class, Object.class, Object[].class); private static final MethodDescriptor PATH_GET_FILENAME = MethodDescriptor.ofMethod(Path.class, "getFileName", Path.class); private static final MethodDescriptor OBJECT_TO_STRING = MethodDescriptor.ofMethod(Object.class, "toString", String.class); @@ -956,7 +958,7 @@ A more full example of generated client (with sub-resource) can is at the bottom Supplier beanParamDescriptorsField = classContext .getLazyBeanParameterDescriptors(beanParam.type); - formParams = addBeanParamData(jandexMethod, methodCreator, handleBeanParamMethod, + formParams = addBeanParamData(jandexMethod, paramIdx, methodCreator, handleBeanParamMethod, invocationBuilderRef, classContext, beanParam.getItems(), methodCreator.getMethodParam(paramIdx), methodTarget, index, restClientInterface.getClassName(), @@ -1024,8 +1026,9 @@ A more full example of generated client (with sub-resource) can is at the bottom } else if (param.parameterType == ParameterType.FORM) { formParams = createFormDataIfAbsent(methodCreator, formParams, multipart); // NOTE: don't use type here, because we're not going through the collection converters and stuff - addFormParam(methodCreator, param.name, methodCreator.getMethodParam(paramIdx), param.declaredType, - param.signature, + Type parameterType = jandexMethod.parameterType(paramIdx); + addFormParam(methodCreator, param.name, methodCreator.getMethodParam(paramIdx), + parameterType, param.declaredType, param.signature, index, restClientInterface.getClassName(), methodCreator.getThis(), formParams, getGenericTypeFromArray(methodCreator, methodGenericParametersField, paramIdx), getAnnotationsFromArray(methodCreator, methodParamAnnotationsField, paramIdx), @@ -1459,7 +1462,7 @@ private void handleSubResourceMethod(List AssignableResultHandle invocationBuilderRef = handleBeanParamMethod .createVariable(Invocation.Builder.class); handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, + formParams = addBeanParamData(jandexMethod, methodIndex, subMethodCreator, handleBeanParamMethod, invocationBuilderRef, subContext, beanParam.getItems(), paramValue, methodTarget, index, interfaceClass.name().toString(), @@ -1586,7 +1589,7 @@ private void handleSubResourceMethod(List AssignableResultHandle invocationBuilderRef = handleBeanParamMethod .createVariable(Invocation.Builder.class); handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, + formParams = addBeanParamData(jandexMethod, methodIndex, subMethodCreator, handleBeanParamMethod, invocationBuilderRef, subContext, beanParam.getItems(), subMethodCreator.getMethodParam(paramIdx), methodTarget, index, interfaceClass.name().toString(), @@ -1771,8 +1774,8 @@ private AssignableResultHandle createRestClientField(String name, ClassCreator c } private void handleMultipartField(String formParamName, String partType, String partFilename, - String type, - String parameterGenericType, ResultHandle fieldValue, AssignableResultHandle multipartForm, + String type, String parameterSignature, + ResultHandle fieldValue, AssignableResultHandle multipartForm, BytecodeCreator methodCreator, ResultHandle client, String restClientInterfaceClassName, ResultHandle parameterAnnotations, ResultHandle genericType, String errorLocation) { @@ -1806,7 +1809,7 @@ private void handleMultipartField(String formParamName, String partType, String MethodDescriptor.ofMethod(Buffer.class, "buffer", Buffer.class, byte[].class), fieldValue); addBuffer(ifValueNotNull, multipartForm, formParamName, partType, partFilename, buffer, errorLocation); - } else if (parameterGenericType.equals(MULTI_BYTE_SIGNATURE)) { + } else if (parameterSignature.equals(MULTI_BYTE_SIGNATURE)) { addMultiAsFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue, errorLocation); } else if (partType != null) { if (partFilename != null) { @@ -2407,7 +2410,7 @@ private Optional getJavaMethod(ClassInfo interfaceClass, ResourceMet } private AssignableResultHandle addBeanParamData(MethodInfo jandexMethod, - BytecodeCreator methodCreator, + int paramIndex, BytecodeCreator methodCreator, // Invocation.Builder executePut$$enrichInvocationBuilder${noOfBeanParam}(Invocation.Builder) BytecodeCreator invocationBuilderEnricher, AssignableResultHandle invocationBuilder, @@ -2429,7 +2432,8 @@ private AssignableResultHandle addBeanParamData(MethodInfo jandexMethod, formParams = createFormDataIfAbsent(methodCreator, formParams, multipart); } - addSubBeanParamData(jandexMethod, methodCreator, invocationBuilderEnricher, invocationBuilder, classContext, + addSubBeanParamData(jandexMethod, paramIndex, methodCreator, invocationBuilderEnricher, invocationBuilder, + classContext, beanParamItems, param, target, index, restClientInterfaceClassName, client, invocationEnricherClient, formParams, descriptorsField, multipart, beanParamClass); @@ -2437,7 +2441,7 @@ private AssignableResultHandle addBeanParamData(MethodInfo jandexMethod, return formParams; } - private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator methodCreator, + private void addSubBeanParamData(MethodInfo jandexMethod, int paramIndex, BytecodeCreator methodCreator, // Invocation.Builder executePut$$enrichInvocationBuilder${noOfBeanParam}(Invocation.Builder) BytecodeCreator invocationBuilderEnricher, AssignableResultHandle invocationBuilder, @@ -2475,7 +2479,7 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method ResultHandle beanParamElementHandle = beanParamItem.extract(creator, param); Supplier newBeanParamDescriptorField = classContext .getLazyBeanParameterDescriptors(beanParamItem.className()); - addSubBeanParamData(jandexMethod, creator, invoEnricher, invocationBuilder, classContext, + addSubBeanParamData(jandexMethod, paramIndex, creator, invoEnricher, invocationBuilder, classContext, beanParamItem.items(), beanParamElementHandle, target, index, restClientInterfaceClassName, client, invocationEnricherClient, formParams, newBeanParamDescriptorField, multipart, @@ -2520,7 +2524,9 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method case FORM_PARAM: FormParamItem formParam = (FormParamItem) item; addFormParam(creator, formParam.getFormParamName(), formParam.extract(creator, param), - formParam.getParamType(), formParam.getParamSignature(), restClientInterfaceClassName, client, + jandexMethod.parameterType(paramIndex), formParam.getParamType(), formParam.getParamSignature(), + index, + restClientInterfaceClassName, client, formParams, getGenericTypeFromParameter(creator, beanParamDescriptorField, item.fieldName()), getAnnotationsFromParameter(creator, beanParamDescriptorField, item.fieldName()), @@ -2786,25 +2792,53 @@ private void addPathParam(BytecodeCreator methodCreator, AssignableResultHandle methodCreator.load(paramName), handle)); } - private void addFormParam(BytecodeCreator methodCreator, String paramName, ResultHandle formParamHandle, - String parameterType, String parameterGenericType, + private void addFormParam(BytecodeCreator methodCreator, + String paramName, + ResultHandle formParamHandle, + Type parameterType, + String parameterTypeStr, + String parameterSignature, + IndexView index, String restClientInterfaceClassName, ResultHandle client, AssignableResultHandle formParams, ResultHandle genericType, ResultHandle parameterAnnotations, boolean multipart, String partType, String partFilename, String errorLocation) { if (multipart) { - handleMultipartField(paramName, partType, partFilename, parameterType, parameterGenericType, formParamHandle, + handleMultipartField(paramName, partType, partFilename, parameterTypeStr, parameterSignature, formParamHandle, formParams, methodCreator, client, restClientInterfaceClassName, parameterAnnotations, genericType, errorLocation); } else { - BytecodeCreator notNullValue = methodCreator.ifNull(formParamHandle).falseBranch(); - ResultHandle convertedFormParam = convertParamToString(notNullValue, client, formParamHandle, parameterType, - genericType, parameterAnnotations); - BytecodeCreator parameterIsStringBranch = checkStringParam(notNullValue, convertedFormParam, - restClientInterfaceClassName, errorLocation); - parameterIsStringBranch.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, - notNullValue.load(paramName), convertedFormParam); + BytecodeCreator creator = methodCreator.ifNull(formParamHandle).falseBranch(); + if (isCollection(parameterType, index)) { + String componentType = null; + if (parameterType.kind() == PARAMETERIZED_TYPE) { + Type paramType = parameterType.asParameterizedType().arguments().get(0); + if ((paramType.kind() == CLASS) || (paramType.kind() == PARAMETERIZED_TYPE)) { + componentType = paramType.name().toString(); + } + } + if (componentType == null) { + componentType = DotNames.OBJECT.toString(); + } + ResultHandle paramArray = creator.invokeStaticMethod( + MethodDescriptor.ofMethod(ToObjectArray.class, "collection", Object[].class, Collection.class), + formParamHandle); + ResultHandle convertedParamArray = creator.invokeVirtualMethod( + MethodDescriptor.ofMethod(RestClientBase.class, "convertParamArray", Object[].class, Object[].class, + Class.class, java.lang.reflect.Type.class, Annotation[].class), + client, paramArray, creator.loadClassFromTCCL(componentType), genericType, creator.newArray( + Annotation.class, 0)); + creator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD_ALL, formParams, + creator.load(paramName), convertedParamArray); + } else { + ResultHandle convertedFormParam = convertParamToString(creator, client, formParamHandle, parameterTypeStr, + genericType, parameterAnnotations); + BytecodeCreator parameterIsStringBranch = checkStringParam(creator, convertedFormParam, + restClientInterfaceClassName, errorLocation); + parameterIsStringBranch.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, + creator.load(paramName), convertedFormParam); + } } } diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormListTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormListTest.java new file mode 100644 index 0000000000000..b05f4c58b8031 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormListTest.java @@ -0,0 +1,51 @@ +package io.quarkus.rest.client.reactive; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.util.List; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.jboss.resteasy.reactive.RestForm; +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 FormListTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(VoidReturnTypeTest.Resource.class)); + + @TestHTTPResource + URI baseUri; + + @Test + void testHeadersWithSubresource() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + assertThat(client.call(List.of("first", "second", "third"))).isEqualTo("first-second-third"); + assertThat(client.call(List.of("first"))).isEqualTo("first"); + } + + @Path("/test") + public static class Resource { + + @POST + public String response(@RestForm List input) { + return String.join("-", input); + } + } + + @Path("/test") + public interface Client { + + @POST + String call(@RestForm List input); + } +} From b128a23fbc6842c13e432b79fb02fe99a4784659 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 11 Apr 2024 13:00:54 +0300 Subject: [PATCH 09/72] Introduce encoding config option for static resources Mentioned in: https://github.com/quarkusio/quarkus/pull/39993#discussion_r1560465576 --- .../quarkus/vertx/http/runtime/StaticResourcesConfig.java | 7 +++++++ .../vertx/http/runtime/StaticResourcesRecorder.java | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java index 94ff031402d81..a2c650a813966 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesConfig.java @@ -1,5 +1,6 @@ package io.quarkus.vertx.http.runtime; +import java.nio.charset.Charset; import java.time.Duration; import io.quarkus.runtime.annotations.ConfigGroup; @@ -50,4 +51,10 @@ public class StaticResourcesConfig { @ConfigItem(defaultValue = "10000") public int maxCacheSize; + /** + * Content encoding for text related files + */ + @ConfigItem(defaultValue = "UTF-8") + public Charset contentEncoding; + } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java index 7738d9bfe34cd..2ef933b783d1c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java @@ -49,7 +49,7 @@ public Consumer start(Set knownPaths) { for (Path resourcePath : hotDeploymentResourcePaths) { String root = resourcePath.toAbsolutePath().toString(); StaticHandler staticHandler = StaticHandler.create(FileSystemAccess.ROOT, root) - .setDefaultContentEncoding("UTF-8") + .setDefaultContentEncoding(config.contentEncoding.name()) .setCachingEnabled(false) .setIndexPage(config.indexPage) .setIncludeHidden(config.includeHidden) From d28045ab0fb1ec85599010097c2009a5b79c4619 Mon Sep 17 00:00:00 2001 From: Laurent Broudoux Date: Thu, 11 Apr 2024 12:31:35 +0200 Subject: [PATCH 10/72] Adding network labeling point from #39156 Signed-off-by: Laurent Broudoux --- .../io/quarkus/devservices/common/ConfigureUtil.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java index 0049cc1348834..fee779b8bfab1 100644 --- a/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java +++ b/extensions/devservices/common/src/main/java/io/quarkus/devservices/common/ConfigureUtil.java @@ -3,17 +3,22 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.utility.Base58; +import com.github.dockerjava.api.command.CreateNetworkCmd; + public final class ConfigureUtil { private static final Map DEVSERVICES_PROPS = new ConcurrentHashMap<>(); @@ -35,6 +40,12 @@ public static String configureSharedNetwork(GenericContainer container, Strin Class networkClass = tccl.getParent() .loadClass("org.testcontainers.containers.Network"); Object sharedNetwork = networkClass.getField("SHARED").get(null); + Consumer addDevservicesLabel = cmd -> cmd + .withLabels(Map.of("quarkus.devservices.network", "shared")); + Field createNetworkCmdModifiersField = sharedNetwork.getClass().getSuperclass() + .getDeclaredField("createNetworkCmdModifiers"); + createNetworkCmdModifiersField.setAccessible(true); + createNetworkCmdModifiersField.set(sharedNetwork, Set.of(addDevservicesLabel)); container.setNetwork((Network) sharedNetwork); } catch (Exception e) { throw new IllegalStateException("Unable to obtain SHARED network from testcontainers", e); From d0c14cd4f87522850bbf5bd58d7f626bf533f430 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Thu, 11 Apr 2024 17:27:32 +0100 Subject: [PATCH 11/72] Formatting only --- .../ExtensionAnnotationProcessor.java | 337 ++++++++++++------ 1 file changed, 222 insertions(+), 115 deletions(-) diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java index 08a043382dbe5..c660290a11c37 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java @@ -130,7 +130,8 @@ public Iterable getCompletions(Element element, Annotation public void doProcess(Set annotations, RoundEnvironment roundEnv) { for (TypeElement annotation : annotations) { - switch (annotation.getQualifiedName().toString()) { + switch (annotation.getQualifiedName() + .toString()) { case Constants.ANNOTATION_BUILD_STEP: trackAnnotationUsed(Constants.ANNOTATION_BUILD_STEP); processBuildStep(roundEnv, annotation); @@ -162,16 +163,19 @@ void doFinish() { try { tempResource = filer.createResource(StandardLocation.SOURCE_OUTPUT, Constants.EMPTY, "ignore.tmp"); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Unable to create temp output file: " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Unable to create temp output file: " + e); return; } final URI uri = tempResource.toUri(); // tempResource.delete(); Path path; try { - path = Paths.get(uri).getParent(); + path = Paths.get(uri) + .getParent(); } catch (RuntimeException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Resource path URI is invalid: " + uri); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Resource path URI is invalid: " + uri); return; } Collection bscListClasses = new TreeSet<>(); @@ -185,7 +189,8 @@ public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttribut } public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) { - final String nameStr = file.getFileName().toString(); + final String nameStr = file.getFileName() + .toString(); if (nameStr.endsWith(".bsc")) { readFile(file, bscListClasses); } else if (nameStr.endsWith(".cr")) { @@ -195,8 +200,9 @@ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attr try (BufferedReader br = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { p.load(br); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to read file " + file + ": " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Failed to read file " + file + ": " + e); } final Set names = p.stringPropertyNames(); for (String name : names) { @@ -208,8 +214,9 @@ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attr } public FileVisitResult visitFileFailed(final Path file, final IOException exc) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to visit file " + file + ": " + exc); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Failed to visit file " + file + ": " + exc); return FileVisitResult.CONTINUE; } @@ -218,7 +225,8 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) } }); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "File walk failed: " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "File walk failed: " + e); } if (!bscListClasses.isEmpty()) try { @@ -226,7 +234,8 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) "META-INF/quarkus-build-steps.list"); writeListResourceFile(bscListClasses, listResource); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write build steps listing: " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to write build steps listing: " + e); return; } if (!crListClasses.isEmpty()) { @@ -235,7 +244,8 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) "META-INF/quarkus-config-roots.list"); writeListResourceFile(crListClasses, listResource); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write config roots listing: " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to write config roots listing: " + e); return; } } @@ -254,7 +264,8 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) } } } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write javadoc properties: " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to write javadoc properties: " + e); return; } } @@ -269,7 +280,8 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) } } } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to generate extension doc: " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to generate extension doc: " + e); return; } @@ -277,8 +289,11 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) private void validateAnnotationUsage() { if (isAnnotationUsed(Constants.ANNOTATION_BUILD_STEP) && isAnnotationUsed(Constants.ANNOTATION_RECORDER)) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Detected use of @Recorder annotation in 'deployment' module. Classes annotated with @Recorder must be part of the extension's 'runtime' module"); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Detected use of @Recorder annotation in 'deployment' module. Classes annotated with @Recorder must be " + + + "part of the extension's 'runtime' module"); } } @@ -315,8 +330,9 @@ private void readFile(Path file, Collection bscListClasses) { } } } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to read file " + file + ": " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Failed to read file " + file + ": " + e); } } @@ -329,29 +345,36 @@ private void processBuildStep(RoundEnvironment roundEnv, TypeElement annotation) continue; } - final PackageElement pkg = processingEnv.getElementUtils().getPackageOf(clazz); + final PackageElement pkg = processingEnv.getElementUtils() + .getPackageOf(clazz); if (pkg == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Element " + clazz + " has no enclosing package"); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Element " + clazz + " has no enclosing package"); continue; } - final String binaryName = processingEnv.getElementUtils().getBinaryName(clazz).toString(); + final String binaryName = processingEnv.getElementUtils() + .getBinaryName(clazz) + .toString(); if (processorClassNames.add(binaryName)) { validateRecordBuildSteps(clazz); recordConfigJavadoc(clazz); generateAccessor(clazz); final StringBuilder rbn = getRelativeBinaryName(clazz, new StringBuilder()); try { - final FileObject itemResource = processingEnv.getFiler().createResource( - StandardLocation.SOURCE_OUTPUT, - pkg.getQualifiedName().toString(), - rbn.toString() + ".bsc", - clazz); + final FileObject itemResource = processingEnv.getFiler() + .createResource( + StandardLocation.SOURCE_OUTPUT, + pkg.getQualifiedName() + .toString(), + rbn.toString() + ".bsc", + clazz); writeResourceFile(binaryName, itemResource); } catch (IOException e1) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to create " + rbn + " in " + pkg + ": " + e1, clazz); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Failed to create " + rbn + " in " + pkg + ": " + e1, clazz); } } } @@ -373,21 +396,28 @@ private void validateRecordBuildSteps(TypeElement clazz) { boolean hasRecorder = false; boolean allTypesResolvable = true; for (VariableElement parameter : ex.getParameters()) { - String parameterClassName = parameter.asType().toString(); - TypeElement parameterTypeElement = processingEnv.getElementUtils().getTypeElement(parameterClassName); + String parameterClassName = parameter.asType() + .toString(); + TypeElement parameterTypeElement = processingEnv.getElementUtils() + .getTypeElement(parameterClassName); if (parameterTypeElement == null) { allTypesResolvable = false; } else { if (isAnnotationPresent(parameterTypeElement, Constants.ANNOTATION_RECORDER)) { - if (parameterTypeElement.getModifiers().contains(Modifier.FINAL)) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Class '" + parameterTypeElement.getQualifiedName() - + "' is annotated with @Recorder and therefore cannot be made as a final class."); + if (parameterTypeElement.getModifiers() + .contains(Modifier.FINAL)) { + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Class '" + parameterTypeElement.getQualifiedName() + + "' is annotated with @Recorder and therefore cannot be made as a final class."); } else if (getPackageName(clazz).equals(getPackageName(parameterTypeElement))) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, - "Build step class '" + clazz.getQualifiedName() - + "' and recorder '" + parameterTypeElement - + "' share the same package. This is highly discouraged as it can lead to unexpected results."); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.WARNING, + "Build step class '" + clazz.getQualifiedName() + + "' and recorder '" + parameterTypeElement + + "' share the same package. This is highly discouraged as it can lead to " + + + "unexpected results."); } hasRecorder = true; break; @@ -396,15 +426,20 @@ private void validateRecordBuildSteps(TypeElement clazz) { } if (!hasRecorder && allTypesResolvable) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Build Step '" + clazz.getQualifiedName() + "#" - + ex.getSimpleName() - + "' which is annotated with '@Record' does not contain a method parameter whose type is annotated with '@Recorder'."); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Build Step '" + clazz.getQualifiedName() + "#" + + ex.getSimpleName() + + "' which is annotated with '@Record' does not contain a method parameter whose type is annotated " + + + "with '@Recorder'."); } } } private Name getPackageName(TypeElement clazz) { - return processingEnv.getElementUtils().getPackageOf(clazz).getQualifiedName(); + return processingEnv.getElementUtils() + .getPackageOf(clazz) + .getQualifiedName(); } private StringBuilder getRelativeBinaryName(TypeElement te, StringBuilder b) { @@ -421,7 +456,8 @@ private TypeElement getClassOf(Element e) { Element t = e; while (!(t instanceof TypeElement)) { if (t == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Element " + e + " has no enclosing class"); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Element " + e + " has no enclosing class"); return null; } t = t.getEnclosingElement(); @@ -430,7 +466,8 @@ private TypeElement getClassOf(Element e) { } private void recordConfigJavadoc(TypeElement clazz) { - String className = clazz.getQualifiedName().toString(); + String className = clazz.getQualifiedName() + .toString(); if (!generatedJavaDocs.add(className)) return; Properties javadocProps = new Properties(); @@ -470,7 +507,8 @@ private void recordConfigJavadoc(TypeElement clazz) { } private void recordMappingJavadoc(TypeElement clazz) { - String className = clazz.getQualifiedName().toString(); + String className = clazz.getQualifiedName() + .toString(); if (!generatedJavaDocs.add(className)) return; if (!isAnnotationPresent(clazz, ANNOTATION_CONFIG_MAPPING)) { @@ -484,7 +522,8 @@ private void recordMappingJavadoc(TypeElement clazz) { } private void recordMappingJavadoc(final TypeElement clazz, final Properties javadocProps) { - String className = clazz.getQualifiedName().toString(); + String className = clazz.getQualifiedName() + .toString(); for (Element e : clazz.getEnclosedElements()) { switch (e.getKind()) { case INTERFACE: { @@ -504,9 +543,11 @@ private void recordMappingJavadoc(final TypeElement clazz, final Properties java } private boolean isEnclosedByMapping(Element clazz) { - if (clazz.getKind().equals(ElementKind.INTERFACE)) { + if (clazz.getKind() + .equals(ElementKind.INTERFACE)) { Element enclosingElement = clazz.getEnclosingElement(); - if (enclosingElement.getKind().equals(ElementKind.INTERFACE)) { + if (enclosingElement.getKind() + .equals(ElementKind.INTERFACE)) { if (isAnnotationPresent(enclosingElement, ANNOTATION_CONFIG_MAPPING)) { return true; } else { @@ -520,30 +561,37 @@ private boolean isEnclosedByMapping(Element clazz) { private void writeJavadocProperties(final TypeElement clazz, final Properties javadocProps) { if (javadocProps.isEmpty()) return; - final PackageElement pkg = processingEnv.getElementUtils().getPackageOf(clazz); - final String rbn = getRelativeBinaryName(clazz, new StringBuilder()).append(".jdp").toString(); + final PackageElement pkg = processingEnv.getElementUtils() + .getPackageOf(clazz); + final String rbn = getRelativeBinaryName(clazz, new StringBuilder()).append(".jdp") + .toString(); try { - FileObject file = processingEnv.getFiler().createResource( - StandardLocation.SOURCE_OUTPUT, - pkg.getQualifiedName().toString(), - rbn, - clazz); + FileObject file = processingEnv.getFiler() + .createResource( + StandardLocation.SOURCE_OUTPUT, + pkg.getQualifiedName() + .toString(), + rbn, + clazz); try (Writer writer = file.openWriter()) { PropertyUtils.store(javadocProps, writer); } } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to persist resource " + rbn + ": " + e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to persist resource " + rbn + ": " + e); } } private void processFieldConfigItem(VariableElement field, Properties javadocProps, String className) { - javadocProps.put(className + Constants.DOT + field.getSimpleName().toString(), getRequiredJavadoc(field)); + javadocProps.put(className + Constants.DOT + field.getSimpleName() + .toString(), getRequiredJavadoc(field)); } private void processEnumConstant(Element field, Properties javadocProps, String className) { String javaDoc = getJavadoc(field); if (javaDoc != null && !javaDoc.isBlank()) { - javadocProps.put(className + Constants.DOT + field.getSimpleName().toString(), javaDoc); + javadocProps.put(className + Constants.DOT + field.getSimpleName() + .toString(), javaDoc); } } @@ -557,20 +605,26 @@ private void processCtorConfigItem(ExecutableElement ctor, Properties javadocPro private void processMethodConfigItem(ExecutableElement method, Properties javadocProps, String className) { final String docComment = getRequiredJavadoc(method); final StringBuilder buf = new StringBuilder(); - buf.append(method.getSimpleName().toString()); + buf.append(method.getSimpleName() + .toString()); appendParamTypes(method, buf); javadocProps.put(className + Constants.DOT + buf, docComment); } private void processMethodConfigMapping(ExecutableElement method, Properties javadocProps, String className) { - if (method.getModifiers().contains(Modifier.ABSTRACT)) { + if (method.getModifiers() + .contains(Modifier.ABSTRACT)) { // Skip toString method, because mappings can include it and generate it - if (method.getSimpleName().contentEquals("toString") && method.getParameters().size() == 0) { + if (method.getSimpleName() + .contentEquals("toString") + && method.getParameters() + .size() == 0) { return; } String docComment = getRequiredJavadoc(method); - javadocProps.put(className + Constants.DOT + method.getSimpleName().toString(), docComment); + javadocProps.put(className + Constants.DOT + method.getSimpleName() + .toString(), docComment); // Find groups without annotation TypeMirror returnType = method.getReturnType(); @@ -593,7 +647,8 @@ private TypeElement unwrapConfigGroup(TypeMirror typeMirror) { } DeclaredType declaredType = (DeclaredType) typeMirror; - String name = declaredType.asElement().toString(); + String name = declaredType.asElement() + .toString(); List typeArguments = declaredType.getTypeArguments(); if (typeArguments.size() == 0) { if (!name.startsWith("java.")) { @@ -616,9 +671,11 @@ private TypeElement unwrapConfigGroup(TypeMirror typeMirror) { private void processConfigGroup(RoundEnvironment roundEnv, TypeElement annotation) { final Set groupClassNames = new HashSet<>(); for (TypeElement i : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { - if (groupClassNames.add(i.getQualifiedName().toString())) { + if (groupClassNames.add(i.getQualifiedName() + .toString())) { generateAccessor(i); - if (isEnclosedByMapping(i) || i.getKind().equals(ElementKind.INTERFACE)) { + if (isEnclosedByMapping(i) || i.getKind() + .equals(ElementKind.INTERFACE)) { recordMappingJavadoc(i); } else { recordConfigJavadoc(i); @@ -634,10 +691,12 @@ private void processConfigRoot(RoundEnvironment roundEnv, TypeElement annotation final Set rootClassNames = new HashSet<>(); for (TypeElement clazz : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { - final PackageElement pkg = processingEnv.getElementUtils().getPackageOf(clazz); + final PackageElement pkg = processingEnv.getElementUtils() + .getPackageOf(clazz); if (pkg == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Element " + clazz + " has no enclosing package"); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Element " + clazz + " has no enclosing package"); continue; } @@ -645,7 +704,9 @@ private void processConfigRoot(RoundEnvironment roundEnv, TypeElement annotation configDocItemScanner.addConfigRoot(pkg, clazz); } - final String binaryName = processingEnv.getElementUtils().getBinaryName(clazz).toString(); + final String binaryName = processingEnv.getElementUtils() + .getBinaryName(clazz) + .toString(); if (rootClassNames.add(binaryName)) { // new class if (isAnnotationPresent(clazz, ANNOTATION_CONFIG_MAPPING)) { @@ -656,15 +717,18 @@ private void processConfigRoot(RoundEnvironment roundEnv, TypeElement annotation } final StringBuilder rbn = getRelativeBinaryName(clazz, new StringBuilder()); try { - final FileObject itemResource = processingEnv.getFiler().createResource( - StandardLocation.SOURCE_OUTPUT, - pkg.getQualifiedName().toString(), - rbn + ".cr", - clazz); + final FileObject itemResource = processingEnv.getFiler() + .createResource( + StandardLocation.SOURCE_OUTPUT, + pkg.getQualifiedName() + .toString(), + rbn + ".cr", + clazz); writeResourceFile(binaryName, itemResource); } catch (IOException e1) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to create " + rbn + " in " + pkg + ": " + e1, clazz); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Failed to create " + rbn + " in " + pkg + ": " + e1, clazz); } } } @@ -686,7 +750,8 @@ private void writeResourceFile(String binaryName, FileObject itemResource) throw private void processRecorder(RoundEnvironment roundEnv, TypeElement annotation) { final Set groupClassNames = new HashSet<>(); for (TypeElement i : typesIn(roundEnv.getElementsAnnotatedWith(annotation))) { - if (groupClassNames.add(i.getQualifiedName().toString())) { + if (groupClassNames.add(i.getQualifiedName() + .toString())) { generateAccessor(i); recordConfigJavadoc(i); } @@ -694,28 +759,35 @@ private void processRecorder(RoundEnvironment roundEnv, TypeElement annotation) } private void generateAccessor(final TypeElement clazz) { - if (!generatedAccessors.add(clazz.getQualifiedName().toString())) + if (!generatedAccessors.add(clazz.getQualifiedName() + .toString())) return; final FormatPreferences fp = new FormatPreferences(); final JSources sources = JDeparser.createSources(JFiler.newInstance(processingEnv.getFiler()), fp); - final PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(clazz); - final String className = getRelativeBinaryName(clazz, new StringBuilder()).append("$$accessor").toString(); - final JSourceFile sourceFile = sources.createSourceFile(packageElement.getQualifiedName().toString(), className); + final PackageElement packageElement = processingEnv.getElementUtils() + .getPackageOf(clazz); + final String className = getRelativeBinaryName(clazz, new StringBuilder()).append("$$accessor") + .toString(); + final JSourceFile sourceFile = sources.createSourceFile(packageElement.getQualifiedName() + .toString(), className); JType clazzType = JTypes.typeOf(clazz.asType()); if (clazz.asType() instanceof DeclaredType) { DeclaredType declaredType = ((DeclaredType) clazz.asType()); TypeMirror enclosingType = declaredType.getEnclosingType(); if (enclosingType != null && enclosingType.getKind() == TypeKind.DECLARED - && clazz.getModifiers().contains(Modifier.STATIC)) { + && clazz.getModifiers() + .contains(Modifier.STATIC)) { // Ugly workaround for Eclipse APT and static nested types clazzType = unnestStaticNestedType(declaredType); } } final JClassDef classDef = sourceFile._class(JMod.PUBLIC | JMod.FINAL, className); classDef.constructor(JMod.PRIVATE); // no construction - classDef.annotate(QUARKUS_GENERATED).value("Quarkus annotation processor"); + classDef.annotate(QUARKUS_GENERATED) + .value("Quarkus annotation processor"); final JAssignableExpr instanceName = JExprs.name(Constants.INSTANCE_SYM); - boolean isEnclosingClassPublic = clazz.getModifiers().contains(Modifier.PUBLIC); + boolean isEnclosingClassPublic = clazz.getModifiers() + .contains(Modifier.PUBLIC); // iterate fields boolean generationNeeded = false; for (VariableElement field : fieldsIn(clazz.getEnclosedElements())) { @@ -733,7 +805,8 @@ private void generateAccessor(final TypeElement clazz) { if (fieldType instanceof DeclaredType) { final DeclaredType declaredType = (DeclaredType) fieldType; final TypeElement typeElement = (TypeElement) declaredType.asElement(); - if (typeElement.getModifiers().contains(Modifier.PUBLIC)) { + if (typeElement.getModifiers() + .contains(Modifier.PUBLIC)) { continue; } } else { @@ -746,24 +819,32 @@ private void generateAccessor(final TypeElement clazz) { final JType realType = JTypes.typeOf(fieldType); final JType publicType = fieldType instanceof PrimitiveType ? realType : JType.OBJECT; - final String fieldName = field.getSimpleName().toString(); + final String fieldName = field.getSimpleName() + .toString(); final JMethodDef getter = classDef.method(JMod.PUBLIC | JMod.STATIC, publicType, "get_" + fieldName); - getter.annotate(SuppressWarnings.class).value("unchecked"); + getter.annotate(SuppressWarnings.class) + .value("unchecked"); getter.param(JType.OBJECT, Constants.INSTANCE_SYM); - getter.body()._return(instanceName.cast(clazzType).field(fieldName)); + getter.body() + ._return(instanceName.cast(clazzType) + .field(fieldName)); final JMethodDef setter = classDef.method(JMod.PUBLIC | JMod.STATIC, JType.VOID, "set_" + fieldName); - setter.annotate(SuppressWarnings.class).value("unchecked"); + setter.annotate(SuppressWarnings.class) + .value("unchecked"); setter.param(JType.OBJECT, Constants.INSTANCE_SYM); setter.param(publicType, fieldName); final JAssignableExpr fieldExpr = JExprs.name(fieldName); - setter.body().assign(instanceName.cast(clazzType).field(fieldName), - (publicType.equals(realType) ? fieldExpr : fieldExpr.cast(realType))); + setter.body() + .assign(instanceName.cast(clazzType) + .field(fieldName), + (publicType.equals(realType) ? fieldExpr : fieldExpr.cast(realType))); } // we need to generate an accessor if the class isn't public if (!isEnclosingClassPublic) { for (ExecutableElement ctor : constructorsIn(clazz.getEnclosedElements())) { - if (ctor.getModifiers().contains(Modifier.PRIVATE)) { + if (ctor.getModifiers() + .contains(Modifier.PRIVATE)) { // skip it continue; } @@ -771,7 +852,9 @@ private void generateAccessor(final TypeElement clazz) { StringBuilder b = new StringBuilder(); for (VariableElement parameter : ctor.getParameters()) { b.append('_'); - b.append(parameter.asType().toString().replace('.', '_')); + b.append(parameter.asType() + .toString() + .replace('.', '_')); } String codedName = b.toString(); final JMethodDef ctorMethod = classDef.method(JMod.PUBLIC | JMod.STATIC, JType.OBJECT, "construct" + codedName); @@ -780,12 +863,14 @@ private void generateAccessor(final TypeElement clazz) { final TypeMirror paramType = parameter.asType(); final JType realType = JTypes.typeOf(paramType); final JType publicType = paramType instanceof PrimitiveType ? realType : JType.OBJECT; - final String name = parameter.getSimpleName().toString(); + final String name = parameter.getSimpleName() + .toString(); ctorMethod.param(publicType, name); final JAssignableExpr nameExpr = JExprs.name(name); ctorCall.arg(publicType.equals(realType) ? nameExpr : nameExpr.cast(realType)); } - ctorMethod.body()._return(ctorCall); + ctorMethod.body() + ._return(ctorCall); } } @@ -794,7 +879,8 @@ private void generateAccessor(final TypeElement clazz) { try { sources.writeSources(); } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to generate source file: " + e, clazz); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Failed to generate source file: " + e, clazz); } } } @@ -802,7 +888,8 @@ private void generateAccessor(final TypeElement clazz) { private JType unnestStaticNestedType(DeclaredType declaredType) { final TypeElement typeElement = (TypeElement) declaredType.asElement(); - final String name = typeElement.getQualifiedName().toString(); + final String name = typeElement.getQualifiedName() + .toString(); final JType rawType = JTypes.typeNamed(name); final List typeArguments = declaredType.getTypeArguments(); if (typeArguments.isEmpty()) { @@ -819,18 +906,25 @@ private JType unnestStaticNestedType(DeclaredType declaredType) { private void appendParamTypes(ExecutableElement ex, final StringBuilder buf) { final List params = ex.getParameters(); if (params.isEmpty()) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Expected at least one parameter", ex); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, "Expected at least one parameter", ex); return; } VariableElement param = params.get(0); DeclaredType dt = (DeclaredType) param.asType(); - String typeName = processingEnv.getElementUtils().getBinaryName(((TypeElement) dt.asElement())).toString(); - buf.append('(').append(typeName); + String typeName = processingEnv.getElementUtils() + .getBinaryName(((TypeElement) dt.asElement())) + .toString(); + buf.append('(') + .append(typeName); for (int i = 1; i < params.size(); ++i) { param = params.get(i); dt = (DeclaredType) param.asType(); - typeName = processingEnv.getElementUtils().getBinaryName(((TypeElement) dt.asElement())).toString(); - buf.append(',').append(typeName); + typeName = processingEnv.getElementUtils() + .getBinaryName(((TypeElement) dt.asElement())) + .toString(); + buf.append(',') + .append(typeName); } buf.append(')'); } @@ -839,15 +933,17 @@ private String getRequiredJavadoc(Element e) { String javaDoc = getJavadoc(e); if (javaDoc == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Unable to find javadoc for config item " + e.getEnclosingElement() + " " + e, e); + processingEnv.getMessager() + .printMessage(Diagnostic.Kind.ERROR, + "Unable to find javadoc for config item " + e.getEnclosingElement() + " " + e, e); return ""; } return javaDoc; } private String getJavadoc(Element e) { - String docComment = processingEnv.getElementUtils().getDocComment(e); + String docComment = processingEnv.getElementUtils() + .getDocComment(e); if (docComment == null) { return null; @@ -855,14 +951,18 @@ private String getJavadoc(Element e) { // javax.lang.model keeps the leading space after the "*" so we need to remove it. - return REMOVE_LEADING_SPACE.matcher(docComment).replaceAll("").trim(); + return REMOVE_LEADING_SPACE.matcher(docComment) + .replaceAll("") + .trim(); } private static boolean isDocumentedConfigItem(Element element) { boolean hasAnnotation = false; for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { - String annotationName = ((TypeElement) annotationMirror.getAnnotationType().asElement()) - .getQualifiedName().toString(); + String annotationName = ((TypeElement) annotationMirror.getAnnotationType() + .asElement()) + .getQualifiedName() + .toString(); if (Constants.ANNOTATION_CONFIG_ITEM.equals(annotationName)) { hasAnnotation = true; Object generateDocumentation = getAnnotationAttribute(annotationMirror, "generateDocumentation()"); @@ -879,8 +979,10 @@ private static boolean isDocumentedConfigItem(Element element) { private static boolean isConfigMappingMethodIgnored(Element element) { for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { - String annotationName = ((TypeElement) annotationMirror.getAnnotationType().asElement()) - .getQualifiedName().toString(); + String annotationName = ((TypeElement) annotationMirror.getAnnotationType() + .asElement()) + .getQualifiedName() + .toString(); if (Constants.ANNOTATION_CONFIG_DOC_IGNORE.equals(annotationName)) { return true; } @@ -890,9 +992,12 @@ private static boolean isConfigMappingMethodIgnored(Element element) { private static Object getAnnotationAttribute(AnnotationMirror annotationMirror, String attributeName) { for (Map.Entry entry : annotationMirror - .getElementValues().entrySet()) { - final String key = entry.getKey().toString(); - final Object value = entry.getValue().getValue(); + .getElementValues() + .entrySet()) { + final String key = entry.getKey() + .toString(); + final Object value = entry.getValue() + .getValue(); if (attributeName.equals(key)) { return value; } @@ -913,7 +1018,9 @@ private static boolean hasParameterDocumentedConfigItem(ExecutableElement ex) { private static boolean isAnnotationPresent(Element element, String... annotationNames) { Set annotations = new HashSet<>(Arrays.asList(annotationNames)); for (AnnotationMirror i : element.getAnnotationMirrors()) { - String annotationName = ((TypeElement) i.getAnnotationType().asElement()).getQualifiedName().toString(); + String annotationName = ((TypeElement) i.getAnnotationType() + .asElement()).getQualifiedName() + .toString(); if (annotations.contains(annotationName)) { return true; } From 47d782f31e222abe19061cf290a9cf2120b502b3 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 11 Apr 2024 18:42:50 +0300 Subject: [PATCH 12/72] Fix broken pre-match test --- ...nHeader.java => PreMatchAcceptInHeaderTest.java} | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) rename independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/{PreMatchAcceptInHeader.java => PreMatchAcceptInHeaderTest.java} (90%) diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeader.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeaderTest.java similarity index 90% rename from independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeader.java rename to independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeaderTest.java index cfbd44a4795fc..eee195df3ce55 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeader.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeaderTest.java @@ -23,15 +23,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -public class PreMatchAcceptInHeader { +public class PreMatchAcceptInHeaderTest { @RegisterExtension static ResteasyReactiveUnitTest test = new ResteasyReactiveUnitTest() .setArchiveProducer(new Supplier<>() { @Override public JavaArchive get() { - return ShrinkWrap.create(JavaArchive.class) - .addClass(PathSegmentTest.Resource.class); + return ShrinkWrap.create(JavaArchive.class); } }); @@ -52,7 +51,7 @@ void text() { .get("test") .then() .statusCode(200) - .body(equalTo("test")); + .body(equalTo("text")); } @Test @@ -62,7 +61,7 @@ void html() { .get("test") .then() .statusCode(200) - .body(equalTo("test")); + .body(containsString("")); } @Test @@ -71,7 +70,7 @@ void json() { .when() .get("test") .then() - .statusCode(404); + .statusCode(406); } @Test @@ -82,7 +81,7 @@ void setAcceptToTextInFilter() { .get("test") .then() .statusCode(200) - .body(equalTo("test")); + .body(equalTo("text")); } @Path("/test") From 4f86368e4f00984558956fdd5ddac9a240c8c78c Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 11 Apr 2024 18:50:49 +0300 Subject: [PATCH 13/72] Take MediaType set in pre-match filter into account during serialization Fixes: #40019 --- .../handlers/VariableProducesHandler.java | 14 ++- .../matching/PreMatchAcceptInHeaderTest.java | 107 ++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/VariableProducesHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/VariableProducesHandler.java index 6c1e712350e67..77faad733c928 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/VariableProducesHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/VariableProducesHandler.java @@ -44,8 +44,18 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti //TODO? return; } - MediaType res = mediaTypeList.negotiateProduces(requestContext.serverRequest().getRequestHeader(HttpHeaders.ACCEPT)) - .getKey(); + MediaType res = null; + List accepts = requestContext.getHttpHeaders().getRequestHeader(HttpHeaders.ACCEPT); + for (String accept : accepts) { + res = mediaTypeList.negotiateProduces(accept).getKey(); + if (res != null) { + break; + } + } + if (res == null) { // fallback for some tests + res = mediaTypeList.negotiateProduces(requestContext.serverRequest().getRequestHeader(HttpHeaders.ACCEPT)) + .getKey(); + } if (res == null) { throw new WebApplicationException(Response .notAcceptable(Variant.mediaTypes(mediaTypeList.getSortedMediaTypes()).build()) diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeaderTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeaderTest.java index eee195df3ce55..16b31cd54b127 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeaderTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PreMatchAcceptInHeaderTest.java @@ -4,11 +4,16 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; import java.util.function.Supplier; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.container.PreMatching; @@ -17,6 +22,9 @@ import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.ext.Provider; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; +import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; @@ -84,6 +92,37 @@ void setAcceptToTextInFilter() { .body(equalTo("text")); } + @Test + void entityJsonWithoutAcceptToTextInFilter() { + given().accept("application/json") + .when() + .get("test/entity") + .then() + .statusCode(200) + .body(containsString("\"text\"")); + } + + @Test + void entityTextWithoutAcceptToTextInFilter() { + given().accept("text/plain") + .when() + .get("test/entity") + .then() + .statusCode(200) + .body(equalTo("text")); + } + + @Test + void entityTextWithAcceptToTextInFilter() { + given().accept("application/json") + .header("x-set-accept-to-text", "true") + .when() + .get("test/entity") + .then() + .statusCode(200) + .body(equalTo("text")); + } + @Path("/test") public static class Resource { @@ -106,6 +145,16 @@ public String html() { """; } + + @GET + @Path("entity") + @Produces({ MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON }) + public Entity entity() { + return new Entity("text"); + } + } + + public record Entity(String value) { } @PreMatching @@ -120,4 +169,62 @@ public void filter(ContainerRequestContext requestContext) { } } } + + @Provider + @Produces(MediaType.TEXT_PLAIN) + public static class DummyTextMessageBodyWriter implements ServerMessageBodyWriter { + + @Override + public boolean isWriteable(Class type, Type genericType, ResteasyReactiveResourceInfo target, + MediaType mediaType) { + return Entity.class.equals(type); + } + + @Override + public void writeResponse(Object o, Type genericType, ServerRequestContext context) + throws WebApplicationException, IOException { + context.serverResponse().end(((Entity) o).value()); + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return Entity.class.equals(type); + } + + @Override + public void writeTo(Object o, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + throw new IllegalStateException("should not be called"); + } + } + + @Provider + @Produces(MediaType.APPLICATION_JSON) + public static class DummyJsonMessageBodyWriter implements ServerMessageBodyWriter { + + @Override + public boolean isWriteable(Class type, Type genericType, ResteasyReactiveResourceInfo target, + MediaType mediaType) { + return Entity.class.equals(type); + } + + @Override + public void writeResponse(Object o, Type genericType, ServerRequestContext context) + throws WebApplicationException, IOException { + context.serverResponse().end("{\"value\":\"" + ((Entity) o).value() + "\"}"); + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return Entity.class.equals(type); + } + + @Override + public void writeTo(Object o, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + throw new IllegalStateException("should not be called"); + } + } } From b9207f73a5e04f47f5e4b5d6afdf6b93f571b333 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Thu, 11 Apr 2024 17:22:11 +0100 Subject: [PATCH 14/72] Add skeleton tests for annotation processor --- core/processor/pom.xml | 11 ++ .../ExtensionAnnotationProcessor.java | 11 +- .../ExtensionAnnotationProcessorTest.java | 76 +++++++++ .../processor/fs/CustomMemoryFileSystem.java | 158 ++++++++++++++++++ .../fs/CustomMemoryFileSystemProvider.java | 152 +++++++++++++++++ .../java.nio.file.spi.FileSystemProvider | 1 + .../deployment/annotations/BuildStep.java | 13 ++ .../org/acme/examples/ArbitraryBuildItem.java | 6 + .../org/acme/examples/ClassWithBuildStep.java | 10 ++ .../acme/examples/ClassWithoutBuildStep.java | 6 + 10 files changed, 437 insertions(+), 7 deletions(-) create mode 100644 core/processor/src/test/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessorTest.java create mode 100644 core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystem.java create mode 100644 core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystemProvider.java create mode 100644 core/processor/src/test/resources/META-INF/services/java.nio.file.spi.FileSystemProvider create mode 100644 core/processor/src/test/resources/io/quarkus/deployment/annotations/BuildStep.java create mode 100644 core/processor/src/test/resources/org/acme/examples/ArbitraryBuildItem.java create mode 100644 core/processor/src/test/resources/org/acme/examples/ClassWithBuildStep.java create mode 100644 core/processor/src/test/resources/org/acme/examples/ClassWithoutBuildStep.java diff --git a/core/processor/pom.xml b/core/processor/pom.xml index 8427a552eb841..aee2ce4090171 100644 --- a/core/processor/pom.xml +++ b/core/processor/pom.xml @@ -59,6 +59,17 @@ jboss-logmanager test + + com.karuslabs + elementary + 2.0.1 + test + + + io.quarkus + quarkus-builder + test + diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java index c660290a11c37..3d978555bbecc 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java @@ -183,7 +183,7 @@ void doFinish() { Properties javaDocProperties = new Properties(); try { - Files.walkFileTree(path, new FileVisitor() { + Files.walkFileTree(path, new FileVisitor<>() { public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) { return FileVisitResult.CONTINUE; } @@ -282,8 +282,6 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) } catch (IOException e) { processingEnv.getMessager() .printMessage(Diagnostic.Kind.ERROR, "Failed to generate extension doc: " + e); - return; - } } @@ -368,8 +366,7 @@ private void processBuildStep(RoundEnvironment roundEnv, TypeElement annotation) StandardLocation.SOURCE_OUTPUT, pkg.getQualifiedName() .toString(), - rbn.toString() + ".bsc", - clazz); + rbn + ".bsc", clazz); writeResourceFile(binaryName, itemResource); } catch (IOException e1) { processingEnv.getMessager() @@ -618,7 +615,7 @@ private void processMethodConfigMapping(ExecutableElement method, Properties jav if (method.getSimpleName() .contentEquals("toString") && method.getParameters() - .size() == 0) { + .isEmpty()) { return; } @@ -650,7 +647,7 @@ private TypeElement unwrapConfigGroup(TypeMirror typeMirror) { String name = declaredType.asElement() .toString(); List typeArguments = declaredType.getTypeArguments(); - if (typeArguments.size() == 0) { + if (typeArguments.isEmpty()) { if (!name.startsWith("java.")) { return (TypeElement) declaredType.asElement(); } diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessorTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessorTest.java new file mode 100644 index 0000000000000..d9d85a19ab53a --- /dev/null +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessorTest.java @@ -0,0 +1,76 @@ +package io.quarkus.annotation.processor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import javax.tools.JavaFileObject; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.karuslabs.elementary.Results; +import com.karuslabs.elementary.junit.JavacExtension; +import com.karuslabs.elementary.junit.annotations.Classpath; +import com.karuslabs.elementary.junit.annotations.Processors; + +import io.quarkus.annotation.processor.fs.CustomMemoryFileSystemProvider; + +@ExtendWith(JavacExtension.class) +@Processors({ ExtensionAnnotationProcessor.class }) +class ExtensionAnnotationProcessorTest { + + @BeforeEach + void beforeEach() { + // This is of limited use, since the filesystem doesn't seem to directly generate files, in the current usage + CustomMemoryFileSystemProvider.reset(); + } + + @Test + @Classpath("org.acme.examples.ClassWithBuildStep") + void shouldProcessClassWithBuildStepWithoutErrors(Results results) throws IOException { + assertNoErrrors(results); + } + + @Test + @Classpath("org.acme.examples.ClassWithBuildStep") + void shouldGenerateABscFile(Results results) throws IOException { + assertNoErrrors(results); + List sources = results.sources; + JavaFileObject bscFile = sources.stream() + .filter(source -> source.getName() + .endsWith(".bsc")) + .findAny() + .orElse(null); + assertNotNull(bscFile); + + String contents = removeLineBreaks(new String(bscFile + .openInputStream() + .readAllBytes(), StandardCharsets.UTF_8)); + assertEquals("org.acme.examples.ClassWithBuildStep", contents); + } + + private String removeLineBreaks(String s) { + return s.replace(System.getProperty("line.separator"), "") + .replace("\n", ""); + } + + @Test + @Classpath("org.acme.examples.ClassWithoutBuildStep") + void shouldProcessEmptyClassWithoutErrors(Results results) { + assertNoErrrors(results); + } + + private static void assertNoErrrors(Results results) { + assertEquals(0, results.find() + .errors() + .count(), + "Errors were: " + results.find() + .errors() + .diagnostics()); + } +} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystem.java b/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystem.java new file mode 100644 index 0000000000000..432bd86b334e9 --- /dev/null +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystem.java @@ -0,0 +1,158 @@ +package io.quarkus.annotation.processor.fs; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.nio.file.WatchService; +import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.spi.FileSystemProvider; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class CustomMemoryFileSystem extends FileSystem { + + private final Map fileContents = new HashMap<>(); + private final CustomMemoryFileSystemProvider provider; + + public CustomMemoryFileSystem(CustomMemoryFileSystemProvider provider) { + this.provider = provider; + } + + @Override + public FileSystemProvider provider() { + return provider; + } + + @Override + public void close() throws IOException { + // No resources to close + } + + @Override + public boolean isOpen() { + return true; // Always open + } + + @Override + public boolean isReadOnly() { + return false; // This filesystem is writable + } + + @Override + public String getSeparator() { + return "/"; // Unix-style separator + } + + @Override + public Iterable getRootDirectories() { + return Collections.singleton(Paths.get("/")); // Single root directory + } + + @Override + public Iterable getFileStores() { + return Collections.emptyList(); // No file stores + } + + @Override + public Set supportedFileAttributeViews() { + return Collections.emptySet(); // No supported file attribute views + } + + @Override + public Path getPath(String first, String... more) { + String path = first; + for (String segment : more) { + path += "/" + segment; + } + return Paths.get(path); + } + + @Override + public PathMatcher getPathMatcher(String syntaxAndPattern) { + return null; + } + + @Override + public UserPrincipalLookupService getUserPrincipalLookupService() { + return null; + } + + @Override + public WatchService newWatchService() throws IOException { + return null; + } + + public void addFile(URI uri, byte[] content) { + fileContents.put(uri, ByteBuffer.wrap(content)); + } + + static class CustomMemorySeekableByteChannel implements SeekableByteChannel { + + private final ByteBuffer buffer; + + CustomMemorySeekableByteChannel(ByteBuffer buffer) { + this.buffer = buffer; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + int remaining = buffer.remaining(); + int count = Math.min(remaining, dst.remaining()); + if (count > 0) { + ByteBuffer slice = buffer.slice(); + slice.limit(count); + dst.put(slice); + buffer.position(buffer.position() + count); + } + return count; + } + + @Override + public int write(ByteBuffer src) throws IOException { + int count = src.remaining(); + buffer.put(src); + return count; + } + + @Override + public long position() throws IOException { + return buffer.position(); + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + buffer.position((int) newPosition); + return this; + } + + @Override + public long size() throws IOException { + return buffer.limit(); + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + buffer.limit((int) size); + return this; + } + + @Override + public boolean isOpen() { + return true; // Always open + } + + @Override + public void close() throws IOException { + // No resources to close + } + } + +} diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystemProvider.java b/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystemProvider.java new file mode 100644 index 0000000000000..8d28a7ae672a6 --- /dev/null +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/fs/CustomMemoryFileSystemProvider.java @@ -0,0 +1,152 @@ +package io.quarkus.annotation.processor.fs; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessMode; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.spi.FileSystemProvider; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class CustomMemoryFileSystemProvider extends FileSystemProvider { + + private static final String MEM = "mem"; + + private static Map fileContents = new HashMap(); + + public static void reset() { + fileContents = new HashMap(); + } + + public static Set getCreatedFiles() { + return fileContents.keySet(); + } + + @Override + public String getScheme() { + return MEM; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + // There's a bit of a disconnect here between the Elementary JavaFileManager and the memory filesystem, + // even though both are in-memory filesystems + return new CustomMemoryFileSystem(this); + } + + @Override + public FileSystem getFileSystem(URI uri) { + throw new UnsupportedOperationException(); + } + + @Override + public Path getPath(URI uri) { + + if (uri.getScheme() == null || !uri.getScheme() + .equalsIgnoreCase(MEM)) { + throw new IllegalArgumentException("For URI " + uri + ", URI scheme is not '" + MEM + "'"); + + } + + // TODO what should we do here? Can we use the java file manager used by Elementary? + try { + return Path.of(File.createTempFile("mem-fs", "adhoc") + .toURI()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) + throws IOException { + if (fileContents.containsKey(path.toUri())) { + ByteBuffer buffer = fileContents.get(path.toUri()); + return new CustomMemoryFileSystem.CustomMemorySeekableByteChannel(buffer); + } else { + throw new NoSuchFileException(path.toString()); + } + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void delete(Path path) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSameFile(Path path1, Path path2) throws IOException { + return path1.equals(path2); + } + + @Override + public boolean isHidden(Path path) throws IOException { + return false; + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + if (!fileContents.containsKey(path.toUri())) { + throw new NoSuchFileException(path.toString()); + } + } + + @Override + public V getFileAttributeView(Path path, Class type, + LinkOption... options) { + throw new UnsupportedOperationException(); + } + + @Override + public A readAttributes(Path path, Class type, LinkOption... options) + throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { + throw new UnsupportedOperationException(); + } +} diff --git a/core/processor/src/test/resources/META-INF/services/java.nio.file.spi.FileSystemProvider b/core/processor/src/test/resources/META-INF/services/java.nio.file.spi.FileSystemProvider new file mode 100644 index 0000000000000..9582882517a77 --- /dev/null +++ b/core/processor/src/test/resources/META-INF/services/java.nio.file.spi.FileSystemProvider @@ -0,0 +1 @@ +io.quarkus.annotation.processor.fs.CustomMemoryFileSystemProvider \ No newline at end of file diff --git a/core/processor/src/test/resources/io/quarkus/deployment/annotations/BuildStep.java b/core/processor/src/test/resources/io/quarkus/deployment/annotations/BuildStep.java new file mode 100644 index 0000000000000..944813a9d720a --- /dev/null +++ b/core/processor/src/test/resources/io/quarkus/deployment/annotations/BuildStep.java @@ -0,0 +1,13 @@ +package io.quarkus.deployment.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface BuildStep { + // FAKE! FAKE! This is only here so we can test without introducing a circular dependency + +} \ No newline at end of file diff --git a/core/processor/src/test/resources/org/acme/examples/ArbitraryBuildItem.java b/core/processor/src/test/resources/org/acme/examples/ArbitraryBuildItem.java new file mode 100644 index 0000000000000..ecb7fb4ee6ee6 --- /dev/null +++ b/core/processor/src/test/resources/org/acme/examples/ArbitraryBuildItem.java @@ -0,0 +1,6 @@ +package org.acme.examples; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class ArbitraryBuildItem extends MultiBuildItem { +} \ No newline at end of file diff --git a/core/processor/src/test/resources/org/acme/examples/ClassWithBuildStep.java b/core/processor/src/test/resources/org/acme/examples/ClassWithBuildStep.java new file mode 100644 index 0000000000000..8dbecc4bff2bc --- /dev/null +++ b/core/processor/src/test/resources/org/acme/examples/ClassWithBuildStep.java @@ -0,0 +1,10 @@ +package org.acme.examples; + +import io.quarkus.deployment.annotations.BuildStep; + +public class ClassWithBuildStep { + @BuildStep + ArbitraryBuildItem feature() { + return new ArbitraryBuildItem(); + } +} diff --git a/core/processor/src/test/resources/org/acme/examples/ClassWithoutBuildStep.java b/core/processor/src/test/resources/org/acme/examples/ClassWithoutBuildStep.java new file mode 100644 index 0000000000000..b40b6d25d2059 --- /dev/null +++ b/core/processor/src/test/resources/org/acme/examples/ClassWithoutBuildStep.java @@ -0,0 +1,6 @@ +package org.acme.examples; + +public class ClassWithoutBuildStep { + + +} From 1fd3f8cc9f3d5655e6b6424bad3437e76a2cf9ce Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Wed, 10 Apr 2024 19:24:37 +0200 Subject: [PATCH 15/72] Make REST Assured version available in the build It is actually used (but missing!) when we build the platform descriptor. Moving it to the parent POM to be available everywhere. --- bom/application/pom.xml | 1 - pom.xml | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index bf068f93dfd8f..bc1f7c1465ea4 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -138,7 +138,6 @@ 10.14.2.0 11.5.8.0 1.2.6 - 5.4.0 2.2 5.10.2 15.0.0.Final diff --git a/pom.xml b/pom.xml index 9398fb32cce29..a6156b23d4f24 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,7 @@ 0.8.12 6.11.0 + 5.4.0 1.62.2 From 77ec8012d859e52691b82164b24fd118cb6784a9 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Thu, 11 Apr 2024 22:51:41 +0200 Subject: [PATCH 16/72] Collect only runtime static resources for native builds --- .../deployment/StaticResourcesProcessor.java | 111 +++++++----------- 1 file changed, 41 insertions(+), 70 deletions(-) diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java index 11bcd0d08eaf8..4371475cfb77e 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java @@ -2,13 +2,7 @@ import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.FileVisitResult; import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -16,17 +10,18 @@ import java.util.Set; import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; -import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; -import io.quarkus.runtime.util.ClassPathUtils; +import io.quarkus.paths.FilteredPathTree; +import io.quarkus.paths.PathFilter; +import io.quarkus.paths.PathVisitor; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.http.deployment.spi.AdditionalStaticResourceBuildItem; import io.quarkus.vertx.http.deployment.spi.StaticResourcesBuildItem; @@ -38,14 +33,14 @@ public class StaticResourcesProcessor { @BuildStep - void collectStaticResources(Capabilities capabilities, ApplicationArchivesBuildItem applicationArchivesBuildItem, + void collectStaticResources(Capabilities capabilities, List additionalStaticResources, - BuildProducer staticResources) throws Exception { + BuildProducer staticResources) { if (capabilities.isPresent(Capability.SERVLET)) { // Servlet container handles static resources return; } - Set paths = getClasspathResources(applicationArchivesBuildItem); + Set paths = getClasspathResources(); for (AdditionalStaticResourceBuildItem bi : additionalStaticResources) { paths.add(new StaticResourcesBuildItem.Entry(bi.getPath(), bi.isDirectory())); } @@ -66,7 +61,7 @@ public void runtimeInit(Optional staticResources, Stat @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) public void nativeImageResource(Optional staticResources, - BuildProducer producer) throws IOException { + BuildProducer producer) { if (staticResources.isPresent()) { Set entries = staticResources.get().getEntries(); List metaInfResources = new ArrayList<>(entries.size()); @@ -83,79 +78,55 @@ public void nativeImageResource(Optional staticResourc // register all directories under META-INF/resources for reflection in order to enable // the serving of index.html in arbitrarily nested directories - ClassPathUtils.consumeAsPaths(StaticResourcesRecorder.META_INF_RESOURCES, resource -> { - try { - Files.walkFileTree(resource, new SimpleFileVisitor<>() { - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { - if (e != null) { - throw e; - } - int index = dir.toString().indexOf(StaticResourcesRecorder.META_INF_RESOURCES); - if (index > 0) { - producer.produce(new NativeImageResourceBuildItem(dir.toString().substring(index))); - } - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - throw new UncheckedIOException(e); + final Set collectedDirs = new HashSet<>(); + visitRuntimeMetaInfResources(visit -> { + if (Files.isDirectory(visit.getPath())) { + final String relativePath = visit.getRelativePath("/"); + if (collectedDirs.add(relativePath)) { + producer.produce(new NativeImageResourceBuildItem(relativePath)); + } } }); - } } /** * Find all static file resources that are available from classpath. * - * @param applicationArchivesBuildItem * @return the set of static resources - * @throws Exception */ - private Set getClasspathResources( - ApplicationArchivesBuildItem applicationArchivesBuildItem) - throws Exception { + private Set getClasspathResources() { Set knownPaths = new HashSet<>(); - - ClassPathUtils.consumeAsPaths(StaticResourcesRecorder.META_INF_RESOURCES, resource -> { - collectKnownPaths(resource, knownPaths); + visitRuntimeMetaInfResources(visit -> { + if (!Files.isDirectory(visit.getPath())) { + knownPaths.add(new StaticResourcesBuildItem.Entry( + visit.getRelativePath("/").substring(StaticResourcesRecorder.META_INF_RESOURCES.length()), + false)); + } }); - - for (ApplicationArchive i : applicationArchivesBuildItem.getAllApplicationArchives()) { - i.accept(tree -> { - Path resource = tree.getPath(StaticResourcesRecorder.META_INF_RESOURCES); - if (resource != null && Files.exists(resource)) { - collectKnownPaths(resource, knownPaths); - } - }); - } - return knownPaths; } - private void collectKnownPaths(Path resource, Set knownPaths) { - try { - Files.walkFileTree(resource, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path p, BasicFileAttributes attrs) - throws IOException { - String file = resource.relativize(p).toString(); - // Windows has a backslash - file = file.replace('\\', '/'); - if (!file.startsWith("/")) { - file = "/" + file; - } - - if (QuarkusClassLoader - .isResourcePresentAtRuntime(StaticResourcesRecorder.META_INF_RESOURCES + file)) { - knownPaths.add(new StaticResourcesBuildItem.Entry(file, false)); - } - return FileVisitResult.CONTINUE; + /** + * Visits all {@code META-INF/resources} directories and their content found on the runtime classpath + * + * @param visitor visitor implementation + */ + private static void visitRuntimeMetaInfResources(PathVisitor visitor) { + final List elements = QuarkusClassLoader.getElements(StaticResourcesRecorder.META_INF_RESOURCES, + false); + if (!elements.isEmpty()) { + final PathFilter filter = PathFilter.forIncludes(List.of( + StaticResourcesRecorder.META_INF_RESOURCES + "/**", + StaticResourcesRecorder.META_INF_RESOURCES)); + for (var element : elements) { + if (element.isRuntime()) { + element.apply(tree -> { + new FilteredPathTree(tree, filter).walk(visitor); + return null; + }); } - }); - } catch (IOException e) { - throw new UncheckedIOException(e); + } } } } From a3291897a287b63f3f36661f5209016668bbe1de Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Fri, 12 Apr 2024 10:55:57 +0200 Subject: [PATCH 17/72] Updates to Infinispan 15.0.1.Final --- bom/application/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 9449c48011b9c..382ec161b8619 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -140,8 +140,8 @@ 1.2.6 2.2 5.10.2 - 15.0.0.Final - 5.0.1.Final + 15.0.1.Final + 5.0.2.Final 3.1.5 4.1.108.Final 1.16.0 From 21e169c36a802d9f283465e3d1004e67e482cdd3 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 12 Apr 2024 12:55:11 +0300 Subject: [PATCH 18/72] Support data.sql in Spring Data JPA module Closes: #40037 --- .../SqlFileConfigBuilderCustomizer.java | 36 ++++++ ...rye.config.SmallRyeConfigBuilderCustomizer | 1 + .../deployment/BothImportAndDataSqlTest.java | 33 +++++ ...ueryWithFlushAndClearUsingDataSqlTest.java | 119 ++++++++++++++++++ .../RepositoryReloadWithDataSqlTest.java | 39 ++++++ .../deployment/src/test/resources/users1.sql | 1 + .../deployment/src/test/resources/users2.sql | 1 + 7 files changed, 230 insertions(+) create mode 100644 extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SqlFileConfigBuilderCustomizer.java create mode 100644 extensions/spring-data-jpa/deployment/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer create mode 100644 extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BothImportAndDataSqlTest.java create mode 100644 extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/ModifyingQueryWithFlushAndClearUsingDataSqlTest.java create mode 100644 extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/devmode/RepositoryReloadWithDataSqlTest.java create mode 100644 extensions/spring-data-jpa/deployment/src/test/resources/users1.sql create mode 100644 extensions/spring-data-jpa/deployment/src/test/resources/users2.sql diff --git a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SqlFileConfigBuilderCustomizer.java b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SqlFileConfigBuilderCustomizer.java new file mode 100644 index 0000000000000..b296ecb9053fe --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SqlFileConfigBuilderCustomizer.java @@ -0,0 +1,36 @@ +package io.quarkus.spring.data.deployment; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; + +public class SqlFileConfigBuilderCustomizer implements SmallRyeConfigBuilderCustomizer { + @Override + public void configBuilder(SmallRyeConfigBuilder builder) { + List supportedSqlFiles = List.of("import.sql", "data.sql"); + List sqlFilesThatExist = new ArrayList<>(); + + for (String sqlFile : supportedSqlFiles) { + URL resource = Thread.currentThread().getContextClassLoader().getResource(sqlFile); + // we only check for files that are part of the application itself, + // this is done as to follow what the HibernateOrmProcessor does + if ((resource != null) && !resource.getProtocol().equals("jar")) { + sqlFilesThatExist.add(sqlFile); + } + } + + // use a priority of 50 to make sure that this is overridable by any of the standard methods + if (!sqlFilesThatExist.isEmpty()) { + builder.withSources( + new PropertiesConfigSource( + Map.of("quarkus.hibernate-orm.sql-load-script", String.join(",", sqlFilesThatExist)), + "quarkus-spring-data-jpa", 50)); + } + + } +} diff --git a/extensions/spring-data-jpa/deployment/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer b/extensions/spring-data-jpa/deployment/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer new file mode 100644 index 0000000000000..5de637f10302e --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/main/resources/META-INF/services/io.smallrye.config.SmallRyeConfigBuilderCustomizer @@ -0,0 +1 @@ +io.quarkus.spring.data.deployment.SqlFileConfigBuilderCustomizer diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BothImportAndDataSqlTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BothImportAndDataSqlTest.java new file mode 100644 index 0000000000000..6a8a6db69b249 --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/BothImportAndDataSqlTest.java @@ -0,0 +1,33 @@ +package io.quarkus.spring.data.deployment; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class BothImportAndDataSqlTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("users1.sql", "data.sql") + .addAsResource("users2.sql", "import.sql") + .addClasses(User.class, LoginEvent.class, UserRepository.class)) + .withConfigurationResource("application.properties"); + + @Inject + UserRepository repo; + + @Test + @Transactional + public void test() { + assertThat(repo.count()).isEqualTo(2); + } +} diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/ModifyingQueryWithFlushAndClearUsingDataSqlTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/ModifyingQueryWithFlushAndClearUsingDataSqlTest.java new file mode 100644 index 0000000000000..d30f2370e34b9 --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/ModifyingQueryWithFlushAndClearUsingDataSqlTest.java @@ -0,0 +1,119 @@ +package io.quarkus.spring.data.deployment; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZonedDateTime; +import java.util.Optional; + +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class ModifyingQueryWithFlushAndClearUsingDataSqlTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("import_users.sql", "data.sql") + .addClasses(User.class, LoginEvent.class, UserRepository.class)) + .withConfigurationResource("application.properties"); + + @Inject + UserRepository repo; + + @BeforeEach + @Transactional + public void setUp() { + final User user = getUser("JOHN"); + user.setLoginCounter(0); + repo.save(user); + } + + @Test + @Transactional + public void testNoAutoClear() { + getUser("JOHN"); // read user to attach it to entity manager + + repo.incrementLoginCounterPlain("JOHN"); + + final User userAfterIncrement = getUser("JOHN"); // we get the cached entity + // the read doesn't re-read the incremented counter and is therefore equal to the old value + assertThat(userAfterIncrement.getLoginCounter()).isEqualTo(0); + } + + @Test + @Transactional + public void testAutoClear() { + getUser("JOHN"); // read user to attach it to entity manager + + repo.incrementLoginCounterAutoClear("JOHN"); + + final User userAfterIncrement = getUser("JOHN"); + assertThat(userAfterIncrement.getLoginCounter()).isEqualTo(1); + } + + @Test + @Transactional + public void testNoAutoFlush() { + final User user = getUser("JOHN"); + createLoginEvent(user); + + repo.processLoginEventsPlain(); + + final User verifyUser = getUser("JOHN"); + // processLoginEvents did not see the new login event + final boolean allProcessed = verifyUser.getLoginEvents().stream() + .allMatch(loginEvent -> loginEvent.isProcessed()); + assertThat(allProcessed).describedAs("all LoginEvents are marked as processed").isFalse(); + } + + @Test + @Transactional + public void testAutoFlush() { + final User user = getUser("JOHN"); + createLoginEvent(user); + + repo.processLoginEventsPlainAutoClearAndFlush(); + + final User verifyUser = getUser("JOHN"); + final boolean allProcessed = verifyUser.getLoginEvents().stream() + .allMatch(loginEvent -> loginEvent.isProcessed()); + assertThat(allProcessed).describedAs("all LoginEvents are marked as processed").isTrue(); + } + + @Test + @Transactional + public void testNamedQueryOnEntities() { + User user = repo.getUserByFullNameUsingNamedQuery("John Doe"); + assertThat(user).isNotNull(); + } + + @Test + @Transactional + public void testNamedQueriesOnEntities() { + User user = repo.getUserByFullNameUsingNamedQueries("John Doe"); + assertThat(user).isNotNull(); + } + + private LoginEvent createLoginEvent(User user) { + final LoginEvent loginEvent = new LoginEvent(); + loginEvent.setUser(user); + loginEvent.setZonedDateTime(ZonedDateTime.now()); + user.addEvent(loginEvent); + return loginEvent; + } + + private User getUser(String userId) { + final Optional user = repo.findById(userId); + assertThat(user).describedAs("user <%s>", userId).isPresent(); + return user.get(); + } + +} diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/devmode/RepositoryReloadWithDataSqlTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/devmode/RepositoryReloadWithDataSqlTest.java new file mode 100644 index 0000000000000..c6c0cf2b0385a --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/devmode/RepositoryReloadWithDataSqlTest.java @@ -0,0 +1,39 @@ +package io.quarkus.spring.data.devmode; + +import static org.hamcrest.CoreMatchers.containsString; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class RepositoryReloadWithDataSqlTest { + + @RegisterExtension + static QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addAsResource("application.properties") + .addAsResource("import_books.sql", "data.sql") + .addClasses(Book.class, BookRepository.class, BookResource.class)); + + @Test + public void testRepositoryIsReloaded() { + RestAssured.get("/book").then() + .statusCode(200) + .body(containsString("Strangers"), containsString("Ascent"), containsString("Everything")); + + TEST.modifySourceFile("BookRepository.java", s -> s.replace("// ", + "java.util.Optional findById(Integer id);")); + + TEST.modifySourceFile("BookResource.java", s -> s.replace("// ", + "@GET @Path(\"/{id}\") @Produces(MediaType.APPLICATION_JSON)\n" + + " public java.util.Optional findById(@jakarta.ws.rs.PathParam(\"id\") Integer id) {\n" + + " return bookRepository.findById(id);\n" + + " }")); + + RestAssured.get("/book/1").then() + .statusCode(200) + .body(containsString("Strangers")); + } +} diff --git a/extensions/spring-data-jpa/deployment/src/test/resources/users1.sql b/extensions/spring-data-jpa/deployment/src/test/resources/users1.sql new file mode 100644 index 0000000000000..d04eefd2895af --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/resources/users1.sql @@ -0,0 +1 @@ +INSERT INTO user_(userid, fullname, logincounter, active) VALUES ('JOHN', 'John Doe', 0, true); diff --git a/extensions/spring-data-jpa/deployment/src/test/resources/users2.sql b/extensions/spring-data-jpa/deployment/src/test/resources/users2.sql new file mode 100644 index 0000000000000..8de8f2dd7c9eb --- /dev/null +++ b/extensions/spring-data-jpa/deployment/src/test/resources/users2.sql @@ -0,0 +1 @@ +INSERT INTO user_(userid, fullname, logincounter, active) VALUES ('JANE', 'Jane Doe', 1, true); From dcb586ecf1a642f73b006e7144e4d268625654e0 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 12 Apr 2024 11:59:08 +0200 Subject: [PATCH 19/72] Qute: do not register TemplateInstance as non-blocking type by default - make it possible to enable the old behavior (for backrward compatibility) - deprecate NonBlockingReturnTypeBuildItem - related to #39971 --- .../qute/deployment/RestQuteConfig.java | 22 +++++++++ .../ResteasyReactiveQuteProcessor.java | 7 ++- ...emplateInstanceNonBlockingEnabledTest.java | 48 +++++++++++++++++++ .../TemplateInstanceNonBlockingTest.java | 3 +- .../spi/NonBlockingReturnTypeBuildItem.java | 3 ++ 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/RestQuteConfig.java create mode 100644 extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingEnabledTest.java diff --git a/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/RestQuteConfig.java b/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/RestQuteConfig.java new file mode 100644 index 0000000000000..840de4a2bee99 --- /dev/null +++ b/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/RestQuteConfig.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.reactive.qute.deployment; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "quarkus.rest.qute") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface RestQuteConfig { + + /** + * If set to {@code true} then the {@link io.quarkus.qute.TemplateInstance} is registered as a non-blocking return type for + * JAX-RS resource methods. + * + * @deprecated This config item will be removed at some time after Quarkus 3.16 + */ + @Deprecated(forRemoval = true, since = "3.10") + @WithDefault("false") + boolean templateInstanceNonBlockingType(); + +} diff --git a/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java b/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java index 59d8213be71a3..137c39476022c 100644 --- a/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java +++ b/extensions/resteasy-reactive/rest-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java @@ -18,6 +18,7 @@ import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem; @@ -49,8 +50,10 @@ ReflectiveHierarchyIgnoreWarningBuildItem ignoreReflectiveWarning() { } @BuildStep - NonBlockingReturnTypeBuildItem nonBlockingTemplateInstance() { - return new NonBlockingReturnTypeBuildItem(TEMPLATE_INSTANCE); + void nonBlockingTemplateInstance(RestQuteConfig config, BuildProducer nonBlockingType) { + if (config.templateInstanceNonBlockingType()) { + nonBlockingType.produce(new NonBlockingReturnTypeBuildItem(TEMPLATE_INSTANCE)); + } } @BuildStep diff --git a/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingEnabledTest.java b/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingEnabledTest.java new file mode 100644 index 0000000000000..d127fa10da991 --- /dev/null +++ b/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingEnabledTest.java @@ -0,0 +1,48 @@ +package io.quarkus.resteasy.reactive.qute.deployment; + +import static io.restassured.RestAssured.when; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.runtime.BlockingOperationControl; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateInstanceNonBlockingEnabledTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestResource.class) + .addAsResource(new StringAsset("quarkus.rest.qute.template-instance-non-blocking-type=true"), + "application.properties") + .addAsResource(new StringAsset("Blocking allowed: {blockingAllowed}"), "templates/item.txt")); + + @Test + public void test() { + when().get("/test").then().statusCode(200).body(Matchers.is("Blocking allowed: false")); + } + + @Path("test") + public static class TestResource { + + @Inject + Template item; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public TemplateInstance get() { + return item.data("blockingAllowed", BlockingOperationControl.isBlockingAllowed()); + } + } +} diff --git a/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingTest.java b/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingTest.java index d1fb7c35fc321..80711c150c370 100644 --- a/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingTest.java +++ b/extensions/resteasy-reactive/rest-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateInstanceNonBlockingTest.java @@ -1,7 +1,6 @@ package io.quarkus.resteasy.reactive.qute.deployment; import static io.restassured.RestAssured.when; -import static org.hamcrest.Matchers.is; import jakarta.inject.Inject; import jakarta.ws.rs.GET; @@ -29,7 +28,7 @@ public class TemplateInstanceNonBlockingTest { @Test public void test() { - when().get("/test").then().statusCode(200).body(Matchers.is("Blocking allowed: false")); + when().get("/test").then().statusCode(200).body(Matchers.is("Blocking allowed: true")); } @Path("test") diff --git a/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/NonBlockingReturnTypeBuildItem.java b/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/NonBlockingReturnTypeBuildItem.java index 7faa3c03802a0..41f9c9fc409dd 100644 --- a/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/NonBlockingReturnTypeBuildItem.java +++ b/extensions/resteasy-reactive/rest/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/NonBlockingReturnTypeBuildItem.java @@ -6,7 +6,10 @@ /** * Register a type as non-blocking by default when used as a return type of JAX-RS Resource + * + * @deprecated This build item will be removed at some time after Quarkus 3.16 */ +@Deprecated(forRemoval = true, since = "3.10") public final class NonBlockingReturnTypeBuildItem extends MultiBuildItem { private final DotName type; From 4c76b0cbdca805ce0b2b5fe9486b4c69499f6b43 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 12 Apr 2024 14:18:52 +0300 Subject: [PATCH 20/72] Notify radcortez on config issues --- .github/quarkus-github-bot.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/quarkus-github-bot.yml b/.github/quarkus-github-bot.yml index 4c62bcbc07290..230db324a202f 100644 --- a/.github/quarkus-github-bot.yml +++ b/.github/quarkus-github-bot.yml @@ -478,6 +478,7 @@ triage: - extensions/config-yaml/ - core/deployment/src/main/java/io/quarkus/deployment/configuration/ - core/runtime/src/main/java/io/quarkus/runtime/configuration/ + notify: [radcortez] - id: core labels: [area/core] notify: [aloubyansky, gsmet, geoand, radcortez, Sanne, stuartwdouglas] From 463a411f31d9eea1abd812427437caef91c412a1 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Tue, 9 Apr 2024 13:02:37 -0300 Subject: [PATCH 21/72] Fix MessageBundle key/file name resolver algorithm --- .../deployment/MessageBundleProcessor.java | 26 ++++++++++- .../MessageBundleProcessorTest.java | 26 +++++++++++ .../qute/deployment/i18n/EmailBundles.java | 45 +++++++++++++++++++ .../i18n/MessageBundleNameCollisionTest.java | 38 ++++++++++++++++ .../messages/EmailBundles_started.properties | 6 +++ .../EmailBundles_startedValidator.properties | 5 +++ ...mailBundles_startedValidator_en.properties | 5 +++ .../EmailBundles_started_en.properties | 6 +++ 8 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/MessageBundleProcessorTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/EmailBundles.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleNameCollisionTest.java create mode 100644 extensions/qute/deployment/src/test/resources/messages/EmailBundles_started.properties create mode 100644 extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator.properties create mode 100644 extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator_en.properties create mode 100644 extensions/qute/deployment/src/test/resources/messages/EmailBundles_started_en.properties 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 a78a3331d11d7..78be8027ee3d0 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 @@ -205,7 +205,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv Map localeToMergeCandidate = new HashMap<>(); for (Path messageFile : messageFiles) { String fileName = messageFile.getFileName().toString(); - if (fileName.startsWith(name)) { + if (bundleNameMatchesFileName(fileName, name)) { final String locale; int postfixIdx = fileName.indexOf('.'); if (postfixIdx == name.length()) { @@ -315,6 +315,30 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv return bundles; } + static boolean bundleNameMatchesFileName(String fileName, String name) { + int fileSeparatorIdx = fileName.indexOf('.'); + // Remove file extension if exists + if (fileSeparatorIdx > -1) { + fileName = fileName.substring(0, fileSeparatorIdx); + } + // Split the filename and the bundle name by underscores + String[] fileNameParts = fileName.split("_"); + String[] nameParts = name.split("_"); + + if (fileNameParts.length < nameParts.length) { + return false; + } + + // Compare each part of the filename with the corresponding part of the bundle name + for (int i = 0; i < nameParts.length; i++) { + if (!fileNameParts[i].equals(nameParts[i])) { + return false; + } + } + + return true; + } + @Record(value = STATIC_INIT) @BuildStep void initBundleContext(MessageBundleRecorder recorder, diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/MessageBundleProcessorTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/MessageBundleProcessorTest.java new file mode 100644 index 0000000000000..4850f984ef46c --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/MessageBundleProcessorTest.java @@ -0,0 +1,26 @@ +package io.quarkus.qute.deployment; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class MessageBundleProcessorTest { + + @Test + void bundleNameMatchesFileName() { + assertTrue(MessageBundleProcessor.bundleNameMatchesFileName("messages.properties", "messages")); + assertTrue(MessageBundleProcessor.bundleNameMatchesFileName("started.properties", "started")); + assertTrue(MessageBundleProcessor.bundleNameMatchesFileName("startedValidation.properties", "startedValidation")); + assertTrue(MessageBundleProcessor.bundleNameMatchesFileName("EmailBundles_startedValidation.properties", + "EmailBundles_startedValidation")); + assertTrue(MessageBundleProcessor.bundleNameMatchesFileName("EmailBundles_startedValidation_pt_BR.properties", + "EmailBundles_startedValidation")); + + assertFalse(MessageBundleProcessor.bundleNameMatchesFileName("startedValidation.properties", "started")); + assertFalse(MessageBundleProcessor.bundleNameMatchesFileName("EmailBundles_startedValidation.properties", + "EmailBundles_started")); + assertFalse(MessageBundleProcessor.bundleNameMatchesFileName("EmailBundles_startedValidation_pt_BR.properties", + "EmailBundles_started")); + } +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/EmailBundles.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/EmailBundles.java new file mode 100644 index 0000000000000..2f7c89a8d0c19 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/EmailBundles.java @@ -0,0 +1,45 @@ +package io.quarkus.qute.deployment.i18n; + +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; + +public class EmailBundles { + @MessageBundle + interface started { + @Message + String started(String id, String filename); + + @Message + String documentAccessUrl(String url); + + @Message + String nextNotification(); + + @Message + String signingProcessStart(String id, String filename); + + @Message + String subject(String customer, String filename); + + @Message + String signForValidation(); + } + + @MessageBundle + interface startedValidator { + @Message + String started(String id, String filename); + + @Message + String turnEmailWillBeSent(); + + @Message + String youMayAlreadyAccessDocument(); + + @Message + String subject(String customer, String filename); + + @Message + String signForValidation(); + } +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleNameCollisionTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleNameCollisionTest.java new file mode 100644 index 0000000000000..6c1d85c11b4c5 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleNameCollisionTest.java @@ -0,0 +1,38 @@ +package io.quarkus.qute.deployment.i18n; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Engine; +import io.quarkus.qute.i18n.MessageBundles; +import io.quarkus.test.QuarkusUnitTest; + +public class MessageBundleNameCollisionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.default-locale", "en_US") + .withApplicationRoot((jar) -> jar + .addClasses(EmailBundles.class) + .addAsResource("messages/EmailBundles_started.properties") + .addAsResource("messages/EmailBundles_started_en.properties") + .addAsResource("messages/EmailBundles_startedValidator.properties") + .addAsResource("messages/EmailBundles_startedValidator_en.properties")); + + @Inject + Engine engine; + + @Test + public void testBundleMethodIsFound() { + EmailBundles.startedValidator startedValidator = MessageBundles.get(EmailBundles.startedValidator.class); + assertEquals("You will be notified with another email when it is your turn to sign.", + startedValidator.turnEmailWillBeSent()); + assertEquals("You will be notified with another email when it is your turn to sign.", + engine.parse("{EmailBundles_startedValidator:turnEmailWillBeSent()}").render()); + } + +} diff --git a/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started.properties b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started.properties new file mode 100644 index 0000000000000..007c4b2517dc8 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started.properties @@ -0,0 +1,6 @@ +started=In this process you will sign the document to validate it. +signingProcessStart=you have started a signing process {id} for document "{filename}". +nextNotification=You will be notified with another email when it is your signing turn. +documentAccessUrl=You may access the document in the following link: +subject=Signing process initiated by {customer} for file {filename}. +signForValidation=In this process you will sign the document to validate it. \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator.properties b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator.properties new file mode 100644 index 0000000000000..88f05a121ad14 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator.properties @@ -0,0 +1,5 @@ +signForValidation=In this process you will sign the document to validate it. +started=has started a signing process {id} for the document "{filename}". +subject=Signing process initiated by {customer} for file {filename}. +turnEmailWillBeSent=You will be notified with another email when it is your turn to sign. +youMayAlreadyAccessDocument=You can access the document at the following link: diff --git a/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator_en.properties b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator_en.properties new file mode 100644 index 0000000000000..79d65ea85fb73 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_startedValidator_en.properties @@ -0,0 +1,5 @@ +signForValidation=In this process you will sign the document to validate it. +started=has started a signing process {id} for the document "{filename}". +subject=Signing process initiated by {customer} for file {filename}. +turnEmailWillBeSent=You will be notified with another email when it is your turn to sign. +youMayAlreadyAccessDocument=You can access the document at the following link: \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started_en.properties b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started_en.properties new file mode 100644 index 0000000000000..007c4b2517dc8 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/EmailBundles_started_en.properties @@ -0,0 +1,6 @@ +started=In this process you will sign the document to validate it. +signingProcessStart=you have started a signing process {id} for document "{filename}". +nextNotification=You will be notified with another email when it is your signing turn. +documentAccessUrl=You may access the document in the following link: +subject=Signing process initiated by {customer} for file {filename}. +signForValidation=In this process you will sign the document to validate it. \ No newline at end of file From abeab0fec0f7517454030ba8c8f5b5facbe821a6 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 9 Apr 2024 20:01:06 +0100 Subject: [PATCH 22/72] Use OAuth2 access token expiry time to set an internal ID token age --- ...ecurity-oidc-code-flow-authentication.adoc | 8 ++- .../quarkus/oidc/AuthorizationCodeTokens.java | 63 ++++++++++++++++++- .../runtime/CodeAuthenticationMechanism.java | 16 +++-- .../oidc/runtime/OidcProviderClient.java | 9 ++- .../keycloak/CodeFlowAuthorizationTest.java | 25 +++++++- 5 files changed, 109 insertions(+), 12 deletions(-) diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc index 6d25021ad93e4..6eb16fe378aef 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -1102,8 +1102,12 @@ To support the integration with such OAuth2 servers, `quarkus-oidc` needs to be [NOTE] ==== Even though you configure the extension to support the authorization code flows without `IdToken`, an internal `IdToken` is generated to standardize the way `quarkus-oidc` operates. -You use an `IdToken` to support the authentication session and to avoid redirecting the user to the provider, such as GitHub, on every request. -In this case, the session lifespan is set to 5 minutes, which you can extend further as described in the <> section. +You use an internal `IdToken` to support the authentication session and to avoid redirecting the user to the provider, such as GitHub, on every request. +In this case, the `IdToken` age is set to the value of a standard `expires_in` property in the authorization code flow response. +You can use a `quarkus.oidc.authentication.internal-id-token-lifespan`property to customize the ID token age. +The default ID token age is 5 minutes. + +, which you can extend further as described in the <> section. This simplifies how you handle an application that supports multiple OIDC providers. ==== diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AuthorizationCodeTokens.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AuthorizationCodeTokens.java index f9e2a1b5d2f53..367ec33bb36ca 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AuthorizationCodeTokens.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/AuthorizationCodeTokens.java @@ -8,37 +8,94 @@ public class AuthorizationCodeTokens { private String idToken; private String accessToken; private String refreshToken; + private Long accessTokenExpiresIn; public AuthorizationCodeTokens() { } public AuthorizationCodeTokens(String idToken, String accessToken, String refreshToken) { - this.setIdToken(idToken); - this.setAccessToken(accessToken); - this.setRefreshToken(refreshToken); + this(idToken, accessToken, refreshToken, null); } + public AuthorizationCodeTokens(String idToken, String accessToken, String refreshToken, Long accessTokenExpiresIn) { + this.idToken = idToken; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.accessTokenExpiresIn = accessTokenExpiresIn; + } + + /** + * Get the ID token + * + * @return ID token + */ public String getIdToken() { return idToken; } + /** + * Set the ID token + * + * @param idToken ID token + */ public void setIdToken(String idToken) { this.idToken = idToken; } + /** + * Get the access token + * + * @return the access token + */ public String getAccessToken() { return accessToken; } + /** + * Set the access token + * + * @param accessToken the access token + */ public void setAccessToken(String accessToken) { this.accessToken = accessToken; } + /** + * Get the refresh token + * + * @return refresh token + */ public String getRefreshToken() { return refreshToken; } + /** + * Set the refresh token + * + * @param refreshToken refresh token + */ public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } + + /** + * Get the access token expires_in value in seconds. + * It is relative to the time the access token is issued at. + * + * @return access token expires_in value in seconds. + */ + public Long getAccessTokenExpiresIn() { + return accessTokenExpiresIn; + } + + /** + * Set the access token expires_in value in seconds. + * It is relative to the time the access token is issued at. + * This property is only checked when an authorization code flow grant completes and does not have to be persisted.. + * + * @param accessTokenExpiresIn access token expires_in value in seconds. + */ + public void setAccessTokenExpiresIn(Long accessTokenExpiresIn) { + this.accessTokenExpiresIn = accessTokenExpiresIn; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index 2c57242392e4f..161fbcd67e884 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -761,7 +761,8 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T LOG.errorf("ID token is not available in the authorization code grant response"); return Uni.createFrom().failure(new AuthenticationCompletionException()); } else { - tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, null)); + tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, null, + tokens.getAccessTokenExpiresIn())); internalIdToken = true; } } else { @@ -788,7 +789,8 @@ public Uni apply(SecurityIdentity identity) { if (internalIdToken && OidcUtils.cacheUserInfoInIdToken(resolver, configContext.oidcConfig)) { tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, - identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE), null)); + identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE), null, + tokens.getAccessTokenExpiresIn())); } return processSuccessfulAuthentication(context, configContext, tokens, idToken, identity); @@ -890,7 +892,8 @@ private CodeAuthenticationStateBean getCodeAuthenticationBean(String[] parsedSta return null; } - private String generateInternalIdToken(OidcTenantConfig oidcConfig, UserInfo userInfo, String currentIdToken) { + private String generateInternalIdToken(OidcTenantConfig oidcConfig, UserInfo userInfo, String currentIdToken, + Long accessTokenExpiresInSecs) { JwtClaimsBuilder builder = Jwt.claims(); if (currentIdToken != null) { AbstractJsonObjectResponse currentIdTokenJson = new AbstractJsonObjectResponse( @@ -908,6 +911,8 @@ private String generateInternalIdToken(OidcTenantConfig oidcConfig, UserInfo use } if (oidcConfig.authentication.internalIdTokenLifespan.isPresent()) { builder.expiresIn(oidcConfig.authentication.internalIdTokenLifespan.get().getSeconds()); + } else if (accessTokenExpiresInSecs != null) { + builder.expiresIn(accessTokenExpiresInSecs); } builder.audience(oidcConfig.getClientId().get()); return builder.jws().header(INTERNAL_IDTOKEN_HEADER, true) @@ -936,7 +941,7 @@ public Uni apply(Void t) { if (configContext.oidcConfig.token.lifespanGrace.isPresent()) { maxAge += configContext.oidcConfig.token.lifespanGrace.getAsInt(); } - if (configContext.oidcConfig.token.refreshExpired) { + if (configContext.oidcConfig.token.refreshExpired && tokens.getRefreshToken() != null) { maxAge += configContext.oidcConfig.authentication.sessionAgeExtension.getSeconds(); } final long sessionMaxAge = maxAge; @@ -1247,7 +1252,8 @@ public AuthorizationCodeTokens apply(AuthorizationCodeTokens tokens) { tokens.setIdToken(currentIdToken); } } else { - tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, currentIdToken)); + tokens.setIdToken(generateInternalIdToken(configContext.oidcConfig, null, currentIdToken, + tokens.getAccessTokenExpiresIn())); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java index 0879f649943bd..0a01e4b8adf9b 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java @@ -211,7 +211,14 @@ private AuthorizationCodeTokens getAuthorizationCodeTokens(HttpResponse final String idToken = json.getString(OidcConstants.ID_TOKEN_VALUE); final String accessToken = json.getString(OidcConstants.ACCESS_TOKEN_VALUE); final String refreshToken = json.getString(OidcConstants.REFRESH_TOKEN_VALUE); - return new AuthorizationCodeTokens(idToken, accessToken, refreshToken); + Long tokenExpiresIn = null; + Object tokenExpiresInObj = json.getValue(OidcConstants.EXPIRES_IN); + if (tokenExpiresInObj != null) { + tokenExpiresIn = tokenExpiresInObj instanceof Number ? ((Number) tokenExpiresInObj).longValue() + : Long.parseLong(tokenExpiresInObj.toString()); + } + + return new AuthorizationCodeTokens(idToken, accessToken, refreshToken, tokenExpiresIn); } private UserInfo getUserInfo(HttpResponse resp) { diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 1064fa118e54f..9f3dd12fd764a 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -284,6 +284,18 @@ public void testCodeFlowUserInfoCachedInIdToken() throws Exception { JsonObject idTokenClaims = decryptIdToken(webClient, "code-flow-user-info-github-cached-in-idtoken"); assertNotNull(idTokenClaims.getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE)); + long issuedAt = idTokenClaims.getLong("iat"); + long expiresAt = idTokenClaims.getLong("exp"); + assertEquals(299, expiresAt - issuedAt); + + Cookie sessionCookie = getSessionCookie(webClient, "code-flow-user-info-github-cached-in-idtoken"); + Date date = sessionCookie.getExpires(); + assertTrue(date.toInstant().getEpochSecond() - issuedAt >= 299 + 300); + // This test enables the token refresh, in this case the cookie age is extended by additional 5 mins + // to minimize the risk of the browser losing immediately after it has expired, for this cookie + // be returned to Quarkus, analyzed and refreshed + assertTrue(date.toInstant().getEpochSecond() - issuedAt <= 299 + 300 + 3); + // refresh Thread.sleep(3000); textPage = webClient.getPage("http://localhost:8081/code-flow-user-info-github-cached-in-idtoken"); @@ -292,6 +304,15 @@ public void testCodeFlowUserInfoCachedInIdToken() throws Exception { idTokenClaims = decryptIdToken(webClient, "code-flow-user-info-github-cached-in-idtoken"); assertNotNull(idTokenClaims.getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE)); + issuedAt = idTokenClaims.getLong("iat"); + expiresAt = idTokenClaims.getLong("exp"); + assertEquals(305, expiresAt - issuedAt); + + sessionCookie = getSessionCookie(webClient, "code-flow-user-info-github-cached-in-idtoken"); + date = sessionCookie.getExpires(); + assertTrue(date.toInstant().getEpochSecond() - issuedAt >= 305 + 300); + assertTrue(date.toInstant().getEpochSecond() - issuedAt <= 305 + 300 + 3); + webClient.getCookieManager().clearCookies(); } @@ -448,6 +469,7 @@ private void defineCodeFlowUserInfoCachedInIdTokenStub() { .withBody("{\n" + " \"access_token\": \"" + OidcWiremockTestResource.getAccessToken("alice", Set.of()) + "\"," + + "\"expires_in\": 299," + " \"refresh_token\": \"refresh1234\"" + "}"))); wireMockServer @@ -464,7 +486,8 @@ private void defineCodeFlowUserInfoCachedInIdTokenStub() { .withHeader("Content-Type", "application/json") .withBody("{\n" + " \"access_token\": \"" - + OidcWiremockTestResource.getAccessToken("bob", Set.of()) + "\"" + + OidcWiremockTestResource.getAccessToken("bob", Set.of()) + "\"," + + "\"expires_in\": 305" + "}"))); } From 27102380b3caa867e195a2ec84c70aeac860d782 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 12 Apr 2024 10:07:27 +0300 Subject: [PATCH 23/72] Improve loading of classes in RunnerClassLoader This ClassLoader is already parallel capable, so just mark it as such. Furthermore, we define the package without using the ClassLoader lock by utilizing getDefinedPackage and checking for duplicate definitions Co-authored-by: David M. Lloyd --- .../bootstrap/runner/RunnerClassLoader.java | 77 ++++++++++++------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java index 3e528969e5899..62c02e8fe08d3 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java @@ -26,6 +26,10 @@ */ public final class RunnerClassLoader extends ClassLoader { + static { + registerAsParallelCapable(); + } + /** * A map of resources by dir name. Root dir/default package is represented by the empty string */ @@ -101,18 +105,55 @@ public Class loadClass(String name, boolean resolve) throws ClassNotFoundExce continue; } definePackage(packageName, resources); - try { - return defineClass(name, data, 0, data.length, resource.getProtectionDomain()); - } catch (LinkageError e) { - loaded = findLoadedClass(name); - if (loaded != null) { - return loaded; + return defineClass(name, data, resource); + } + } + return getParent().loadClass(name); + } + + private void definePackage(String pkgName, ClassLoadingResource[] resources) { + if ((pkgName != null) && getDefinedPackage(pkgName) == null) { + for (ClassLoadingResource classPathElement : resources) { + ManifestInfo mf = classPathElement.getManifestInfo(); + if (mf != null) { + try { + definePackage(pkgName, mf.getSpecTitle(), + mf.getSpecVersion(), + mf.getSpecVendor(), + mf.getImplTitle(), + mf.getImplVersion(), + mf.getImplVendor(), null); + } catch (IllegalArgumentException e) { + var loaded = getDefinedPackage(pkgName); + if (loaded == null) { + throw e; + } } + return; + } + } + try { + definePackage(pkgName, null, null, null, null, null, null, null); + } catch (IllegalArgumentException e) { + var loaded = getDefinedPackage(pkgName); + if (loaded == null) { throw e; } } } - return getParent().loadClass(name); + } + + private Class defineClass(String name, byte[] data, ClassLoadingResource resource) { + Class loaded; + try { + return defineClass(name, data, 0, data.length, resource.getProtectionDomain()); + } catch (LinkageError e) { + loaded = findLoadedClass(name); + if (loaded != null) { + return loaded; + } + throw e; + } } private void accessingResource(final ClassLoadingResource resource) { @@ -219,28 +260,6 @@ protected Enumeration findResources(String name) { return Collections.enumeration(urls); } - private void definePackage(String pkgName, ClassLoadingResource[] resources) { - if ((pkgName != null) && getPackage(pkgName) == null) { - synchronized (getClassLoadingLock(pkgName)) { - if (getPackage(pkgName) == null) { - for (ClassLoadingResource classPathElement : resources) { - ManifestInfo mf = classPathElement.getManifestInfo(); - if (mf != null) { - definePackage(pkgName, mf.getSpecTitle(), - mf.getSpecVersion(), - mf.getSpecVendor(), - mf.getImplTitle(), - mf.getImplVersion(), - mf.getImplVendor(), null); - return; - } - } - definePackage(pkgName, null, null, null, null, null, null, null); - } - } - } - } - private String getPackageNameFromClassName(String className) { final int index = className.lastIndexOf('.'); if (index == -1) { From 6bf95c6c89ac441cc2be7718c69ac6926d11d22c Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 12 Apr 2024 16:03:52 +0300 Subject: [PATCH 24/72] Mention data.sql in Spring Data JPA docs --- docs/src/main/asciidoc/spring-data-jpa.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/main/asciidoc/spring-data-jpa.adoc b/docs/src/main/asciidoc/spring-data-jpa.adoc index 201ae4df8ab29..cb41a8bc350d0 100644 --- a/docs/src/main/asciidoc/spring-data-jpa.adoc +++ b/docs/src/main/asciidoc/spring-data-jpa.adoc @@ -162,6 +162,8 @@ INSERT INTO fruit(id, name, color) VALUES (5, 'Strawberry', 'Red'); Hibernate ORM will execute these queries on application startup. +TIP: Users can also use a file named `data.sql` in addition to `import.sql` + == Define the repository It is now time to define the repository that will be used to access `Fruit`. From 2cde5a66a50489867f9fb32a6dbce51992216b15 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Tue, 2 Apr 2024 16:24:31 +0200 Subject: [PATCH 25/72] ArC: replace thread locals with fields in CreationalContextImpl --- .../arc/runtime/ConfigBeanCreator.java | 2 +- .../ConfigStaticInitCheckInterceptor.java | 13 +++-- .../runtime/auth/RawOptionalClaimCreator.java | 2 +- .../quarkus/arc/processor/BeanGenerator.java | 52 +++++++++++++++---- .../arc/processor/DecoratorGenerator.java | 17 +++++- .../arc/processor/MethodDescriptors.java | 11 ++-- .../arc/processor/SubclassGenerator.java | 6 ++- .../io/quarkus/arc/impl/ArcContainerImpl.java | 4 +- .../io/quarkus/arc/impl/BeanManagerImpl.java | 8 +-- .../arc/impl/CreationalContextImpl.java | 50 ++++++++++++++++++ .../impl/CurrentInjectionPointProvider.java | 4 +- .../arc/impl/DecoratorDelegateProvider.java | 36 ++++--------- .../java/io/quarkus/arc/impl/EventBean.java | 2 +- .../quarkus/arc/impl/InjectionPointBean.java | 2 +- .../arc/impl/InjectionPointProvider.java | 32 ++++-------- .../io/quarkus/arc/impl/InstanceBean.java | 2 +- .../io/quarkus/arc/impl/InstanceImpl.java | 8 +-- .../java/io/quarkus/arc/impl/Instances.java | 12 ++--- 18 files changed, 166 insertions(+), 97 deletions(-) diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java index 548d2afe985c2..03b232b3111bc 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ConfigBeanCreator.java @@ -27,7 +27,7 @@ public Object create(CreationalContext creationalContext, Map> { @Override public Optional create(CreationalContext> creationalContext, Map params) { - InjectionPoint injectionPoint = InjectionPointProvider.get(); + InjectionPoint injectionPoint = InjectionPointProvider.getCurrent(creationalContext); if (injectionPoint == null) { throw new IllegalStateException("No current injection point found"); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java index c9df9d7965a50..0e94f7df30f00 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java @@ -1305,26 +1305,45 @@ private ResultHandle newInstanceHandle(BeanInfo bean, ClassCreator beanCreator, if (Modifier.isPrivate(constructor.flags())) { privateMembers.add(isApplicationClass, String.format("Bean constructor %s on %s", constructor, constructor.declaringClass().name())); - ResultHandle paramTypesArray = creator.newArray(Class.class, creator.load(providerHandles.size())); - ResultHandle argsArray = creator.newArray(Object.class, creator.load(providerHandles.size())); + int params = providerHandles.size(); + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + params++; + } + ResultHandle paramTypesArray = creator.newArray(Class.class, creator.load(params)); + ResultHandle argsArray = creator.newArray(Object.class, creator.load(params)); for (int i = 0; i < injectionPoints.size(); i++) { creator.writeArrayValue(paramTypesArray, i, creator.loadClass(injectionPoints.get(i).getType().name().toString())); creator.writeArrayValue(argsArray, i, providerHandles.get(i)); } + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + creator.writeArrayValue(paramTypesArray, params - 1, creator.loadClass(CreationalContext.class)); + creator.writeArrayValue(argsArray, params - 1, createMethod.getMethodParam(0)); + } registration.registerMethod(constructor); return creator.invokeStaticMethod(MethodDescriptors.REFLECTIONS_NEW_INSTANCE, creator.loadClass(constructor.declaringClass().name().toString()), paramTypesArray, argsArray); } else { // new SimpleBean(foo) - String[] paramTypes = new String[injectionPoints.size()]; + int params = injectionPoints.size(); + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + params++; + } + String[] paramTypes = new String[params]; for (ListIterator iterator = injectionPoints.listIterator(); iterator.hasNext();) { InjectionPointInfo injectionPoint = iterator.next(); paramTypes[iterator.previousIndex()] = DescriptorUtils.typeToString(injectionPoint.getType()); } - return creator.newInstance(MethodDescriptor.ofConstructor(providerTypeName, paramTypes), - providerHandles.toArray(new ResultHandle[0])); + ResultHandle[] args = new ResultHandle[params]; + for (int i = 0; i < providerHandles.size(); i++) { + args[i] = providerHandles.get(i); + } + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + paramTypes[params - 1] = CreationalContext.class.getName(); + args[params - 1] = createMethod.getMethodParam(0); + } + return creator.newInstance(MethodDescriptor.ofConstructor(providerTypeName, paramTypes), args); } } else { MethodInfo noArgsConstructor = bean.getTarget().get().asClass().method(Methods.INIT); @@ -1332,16 +1351,31 @@ private ResultHandle newInstanceHandle(BeanInfo bean, ClassCreator beanCreator, privateMembers.add(isApplicationClass, String.format("Bean constructor %s on %s", noArgsConstructor, noArgsConstructor.declaringClass().name())); - ResultHandle paramTypesArray = creator.newArray(Class.class, creator.load(0)); - ResultHandle argsArray = creator.newArray(Object.class, creator.load(0)); + ResultHandle paramTypesArray; + ResultHandle argsArray; + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + paramTypesArray = creator.newArray(Class.class, 1); + argsArray = creator.newArray(Object.class, 1); + creator.writeArrayValue(paramTypesArray, 0, creator.loadClass(CreationalContext.class)); + creator.writeArrayValue(argsArray, 0, createMethod.getMethodParam(0)); + } else { + paramTypesArray = creator.newArray(Class.class, 0); + argsArray = creator.newArray(Object.class, 0); + } registration.registerMethod(noArgsConstructor); return creator.invokeStaticMethod(MethodDescriptors.REFLECTIONS_NEW_INSTANCE, creator.loadClass(noArgsConstructor.declaringClass().name().toString()), paramTypesArray, argsArray); } else { - // new SimpleBean() - return creator.newInstance(MethodDescriptor.ofConstructor(providerTypeName)); + if (DecoratorGenerator.isAbstractDecoratorImpl(bean, providerTypeName)) { + // new SimpleDecorator_Impl(ctx) + return creator.newInstance(MethodDescriptor.ofConstructor(providerTypeName, CreationalContext.class), + createMethod.getMethodParam(0)); + } else { + // new SimpleBean() + return creator.newInstance(MethodDescriptor.ofConstructor(providerTypeName)); + } } } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java index 48c92af011388..97a4bbd430776 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java @@ -5,6 +5,7 @@ import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -16,6 +17,8 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import jakarta.enterprise.context.spi.CreationalContext; + import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -157,6 +160,10 @@ static String createBaseName(ClassInfo decoratorClass) { return baseName; } + static boolean isAbstractDecoratorImpl(BeanInfo bean, String providerTypeName) { + return bean.isDecorator() && ((DecoratorInfo) bean).isAbstract() && providerTypeName.endsWith(ABSTRACT_IMPL_SUFFIX); + } + private String generateDecoratorImplementation(String baseName, String targetPackage, DecoratorInfo decorator, ClassInfo decoratorClass, ClassOutput classOutput) { // MyDecorator_Impl @@ -171,8 +178,13 @@ private String generateDecoratorImplementation(String baseName, String targetPac // Constructor MethodInfo decoratorConstructor = decoratorClass.firstMethod(Methods.INIT); + List decoratorConstructorParams = new ArrayList<>(); + for (org.jboss.jandex.Type parameterType : decoratorConstructor.parameterTypes()) { + decoratorConstructorParams.add(parameterType.name().toString()); + } + decoratorConstructorParams.add(CreationalContext.class.getName()); MethodCreator constructor = decoratorImplCreator.getMethodCreator(Methods.INIT, "V", - decoratorConstructor.parameterTypes().stream().map(it -> it.name().toString()).toArray()); + decoratorConstructorParams.toArray(new Object[0])); ResultHandle[] constructorArgs = new ResultHandle[decoratorConstructor.parametersCount()]; for (int i = 0; i < decoratorConstructor.parametersCount(); i++) { constructorArgs[i] = constructor.getMethodParam(i); @@ -181,7 +193,8 @@ private String generateDecoratorImplementation(String baseName, String targetPac constructor.invokeSpecialMethod(decoratorConstructor, constructor.getThis(), constructorArgs); // Set the delegate field constructor.writeInstanceField(delegateField.getFieldDescriptor(), constructor.getThis(), - constructor.invokeStaticMethod(MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_GET)); + constructor.invokeStaticMethod(MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_GET, + constructor.getMethodParam(decoratorConstructor.parametersCount()))); constructor.returnValue(null); // Find non-decorated methods from all decorated types diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java index 79b2f7af860b2..ebccb00134a45 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java @@ -287,14 +287,11 @@ public final class MethodDescriptors { public static final MethodDescriptor CLIENT_PROXIES_GET_DELEGATE = MethodDescriptor.ofMethod(ClientProxies.class, "getDelegate", Object.class, InjectableBean.class); - public static final MethodDescriptor DECORATOR_DELEGATE_PROVIDER_SET = MethodDescriptor - .ofMethod(DecoratorDelegateProvider.class, "set", Object.class, Object.class); + public static final MethodDescriptor DECORATOR_DELEGATE_PROVIDER_GET = MethodDescriptor.ofMethod( + DecoratorDelegateProvider.class, "getCurrent", Object.class, CreationalContext.class); - public static final MethodDescriptor DECORATOR_DELEGATE_PROVIDER_UNSET = MethodDescriptor - .ofMethod(DecoratorDelegateProvider.class, "unset", void.class); - - public static final MethodDescriptor DECORATOR_DELEGATE_PROVIDER_GET = MethodDescriptor - .ofMethod(DecoratorDelegateProvider.class, "get", Object.class); + public static final MethodDescriptor DECORATOR_DELEGATE_PROVIDER_SET = MethodDescriptor.ofMethod( + DecoratorDelegateProvider.class, "setCurrent", Object.class, CreationalContext.class, Object.class); public static final MethodDescriptor INSTANCES_LIST_OF = MethodDescriptor .ofMethod(Instances.class, "listOf", List.class, InjectableBean.class, Type.class, Type.class, diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java index 1574f72bc58be..04b8bc44075ef 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java @@ -817,12 +817,14 @@ && isDecorated(decoratedMethodDescriptors, methodDescriptor, resolvedMethodDescr } ResultHandle delegateSubclassInstance = subclassConstructor.newInstance(MethodDescriptor.ofConstructor( delegateSubclass.getClassName(), constructorParameterTypes.toArray(new String[0])), paramHandles); - subclassConstructor.invokeStaticMethod(MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_SET, delegateSubclassInstance); + ResultHandle prev = subclassConstructor.invokeStaticMethod( + MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_SET, creationalContext, delegateSubclassInstance); ResultHandle decoratorInstance = subclassConstructor.invokeInterfaceMethod( MethodDescriptors.INJECTABLE_REF_PROVIDER_GET, constructorMethodParam, creationalContext); // And unset the delegate IP afterwards - subclassConstructor.invokeStaticMethod(MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_UNSET); + subclassConstructor.invokeStaticMethod( + MethodDescriptors.DECORATOR_DELEGATE_PROVIDER_SET, creationalContext, prev); decoratorToResultHandle.put(decorator.getIdentifier(), decoratorInstance); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java index 94d6c17e59d12..25e8e75258b5d 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java @@ -553,14 +553,14 @@ static InstanceHandle beanInstanceHandle(InjectableBean bean, Creation } InjectionPoint prev = null; if (resetCurrentInjectionPoint) { - prev = InjectionPointProvider.set(CurrentInjectionPointProvider.EMPTY); + prev = InjectionPointProvider.setCurrent(creationalContext, CurrentInjectionPointProvider.EMPTY); } try { return new EagerInstanceHandle<>(bean, bean.get(creationalContext), creationalContext, parentContext, destroyLogic); } finally { if (resetCurrentInjectionPoint) { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(creationalContext, prev); } } } else { diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java index 72d4853101f83..9527ac27ae1c5 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java @@ -62,12 +62,12 @@ public Object getReference(Bean bean, Type beanType, CreationalContext ctx if (bean instanceof InjectableBean && ctx instanceof CreationalContextImpl) { // there's no actual injection point or an `Instance` object, // the "current" injection point must be `null` - InjectionPoint prev = InjectionPointProvider.set(null); + InjectionPoint prev = InjectionPointProvider.setCurrent(ctx, null); try { return ArcContainerImpl.beanInstanceHandle((InjectableBean) bean, (CreationalContextImpl) ctx, false, null, true).get(); } finally { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(ctx, prev); } } throw new IllegalArgumentException( @@ -86,12 +86,12 @@ public Object getInjectableReference(InjectionPoint ij, CreationalContext ctx throw new UnsatisfiedResolutionException(); } InjectableBean bean = (InjectableBean) resolve(beans); - InjectionPoint prev = InjectionPointProvider.set(ij); + InjectionPoint prev = InjectionPointProvider.setCurrent(ctx, ij); try { return ArcContainerImpl.beanInstanceHandle(bean, (CreationalContextImpl) ctx, false, null, true).get(); } finally { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(ctx, prev); } } throw new IllegalArgumentException( diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CreationalContextImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CreationalContextImpl.java index 5de156b6170d5..43b6c612db3c8 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CreationalContextImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CreationalContextImpl.java @@ -7,6 +7,7 @@ import jakarta.enterprise.context.spi.Contextual; import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.InjectionPoint; import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InjectableReferenceProvider; @@ -24,6 +25,9 @@ public class CreationalContextImpl implements CreationalContext, Function< private final CreationalContextImpl parent; private List> dependentInstances; + private InjectionPoint currentInjectionPoint; + private Object currentDecoratorDelegate; + public CreationalContextImpl(Contextual contextual) { this(contextual, null); } @@ -129,4 +133,50 @@ public static void addDependencyToParent(InjectableBean bean, I instance, } } + static InjectionPoint getCurrentInjectionPoint(CreationalContext ctx) { + CreationalContextImpl instance = unwrap(ctx); + while (instance != null) { + synchronized (instance) { + InjectionPoint result = instance.currentInjectionPoint; + if (result != null) { + return result; + } + } + instance = instance.parent; + } + return null; + } + + static InjectionPoint setCurrentInjectionPoint(CreationalContext ctx, InjectionPoint injectionPoint) { + CreationalContextImpl instance = unwrap(ctx); + synchronized (instance) { + InjectionPoint previous = instance.currentInjectionPoint; + instance.currentInjectionPoint = injectionPoint; + return previous; + } + } + + static Object getCurrentDecoratorDelegate(CreationalContext ctx) { + CreationalContextImpl instance = unwrap(ctx); + while (instance != null) { + synchronized (instance) { + Object result = instance.currentDecoratorDelegate; + if (result != null) { + return result; + } + } + instance = instance.parent; + } + return null; + } + + static Object setCurrentDecoratorDelegate(CreationalContext ctx, Object decoratorDelegate) { + CreationalContextImpl instance = unwrap(ctx); + synchronized (instance) { + Object previous = instance.currentDecoratorDelegate; + instance.currentDecoratorDelegate = decoratorDelegate; + return previous; + } + } + } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java index 6b3f020fc9481..76a368267c472 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/CurrentInjectionPointProvider.java @@ -43,11 +43,11 @@ public CurrentInjectionPointProvider(InjectableBean bean, Supplier creationalContext) { - InjectionPoint prev = InjectionPointProvider.set(injectionPoint); + InjectionPoint prev = InjectionPointProvider.setCurrent(creationalContext, injectionPoint); try { return delegateSupplier.get().get(creationalContext); } finally { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(creationalContext, prev); } } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratorDelegateProvider.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratorDelegateProvider.java index d7ef2fede1a11..f548a6087b6a4 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratorDelegateProvider.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratorDelegateProvider.java @@ -6,40 +6,24 @@ public class DecoratorDelegateProvider implements InjectableReferenceProvider { - private static final ThreadLocal CURRENT = new ThreadLocal<>(); - @Override public Object get(CreationalContext creationalContext) { - return CURRENT.get(); + return getCurrent(creationalContext); + } + + public static Object getCurrent(CreationalContext ctx) { + return CreationalContextImpl.getCurrentDecoratorDelegate(ctx); } /** - * Set the current delegate for a non-null parameter, remove the threadlocal for null parameter. + * Set the current delegate for a non-null parameter, or remove it for null parameter. * - * @param delegate * @return the previous delegate or {@code null} */ - public static Object set(Object delegate) { - if (delegate != null) { - Object prev = CURRENT.get(); - if (delegate.equals(prev)) { - return delegate; - } else { - CURRENT.set(delegate); - return prev; - } - } else { - CURRENT.remove(); - return null; - } - } - - public static void unset() { - set(null); - } - - public static Object get() { - return CURRENT.get(); + public static Object setCurrent(CreationalContext ctx, Object delegate) { + // it wouldn't be necessary to reset this, but we do that as a safeguard, + // to prevent accidental references from keeping these objects alive + return CreationalContextImpl.setCurrentDecoratorDelegate(ctx, delegate); } } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/EventBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/EventBean.java index a67ea603cc11d..0b1a67c849e3a 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/EventBean.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/EventBean.java @@ -19,7 +19,7 @@ public Set getTypes() { @Override public Event get(CreationalContext> creationalContext) { // Obtain current IP to get the required type and qualifiers - InjectionPoint ip = InjectionPointProvider.get(); + InjectionPoint ip = InjectionPointProvider.getCurrent(creationalContext); return new EventImpl<>(ip.getType(), ip.getQualifiers(), ip); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointBean.java index 0163206d6ca7c..7cb2c87660d91 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointBean.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointBean.java @@ -16,7 +16,7 @@ public Set getTypes() { @Override public InjectionPoint get(CreationalContext creationalContext) { - return InjectionPointProvider.get(); + return InjectionPointProvider.getCurrent(creationalContext); } @Override diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointProvider.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointProvider.java index 5dd28847b94b0..081d16751e608 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointProvider.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InjectionPointProvider.java @@ -11,36 +11,24 @@ */ public class InjectionPointProvider implements InjectableReferenceProvider { - private static final ThreadLocal CURRENT = new ThreadLocal<>(); - @Override public InjectionPoint get(CreationalContext creationalContext) { - return CURRENT.get(); + return getCurrent(creationalContext); + } + + public static InjectionPoint getCurrent(CreationalContext ctx) { + return CreationalContextImpl.getCurrentInjectionPoint(ctx); } /** - * Set the current injection point for a non-null parameter, remove the threadlocal for null parameter. + * Set the current injection point for a non-null parameter, or remove it for null parameter. * - * @param injectionPoint * @return the previous injection point or {@code null} */ - static InjectionPoint set(InjectionPoint injectionPoint) { - if (injectionPoint != null) { - InjectionPoint prev = InjectionPointProvider.CURRENT.get(); - if (injectionPoint.equals(prev)) { - return injectionPoint; - } else { - InjectionPointProvider.CURRENT.set(injectionPoint); - return prev; - } - } else { - CURRENT.remove(); - return null; - } - } - - public static InjectionPoint get() { - return CURRENT.get(); + static InjectionPoint setCurrent(CreationalContext ctx, InjectionPoint ip) { + // it wouldn't be necessary to reset this, but we do that as a safeguard, + // to prevent accidental references from keeping these objects alive + return CreationalContextImpl.setCurrentInjectionPoint(ctx, ip); } } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceBean.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceBean.java index 0fc33ce7e6c96..7580d82d48e39 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceBean.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceBean.java @@ -30,7 +30,7 @@ public Class getBeanClass() { @Override public Instance get(CreationalContext> creationalContext) { // Obtain current IP to get the required type and qualifiers - InjectionPoint ip = InjectionPointProvider.get(); + InjectionPoint ip = InjectionPointProvider.getCurrent(creationalContext); InstanceImpl> instance = InstanceImpl.forInjection((InjectableBean) ip.getBean(), ip.getType(), ip.getQualifiers(), (CreationalContextImpl) creationalContext, Collections.EMPTY_SET, ip.getMember(), 0, ip.isTransient()); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java index 4c84a064e46d4..2ce4864251c0a 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java @@ -264,14 +264,14 @@ private InstanceHandle getHandle(InjectableBean bean) { public H get() { InjectionPoint prev = null; if (resetCurrentInjectionPoint) { - prev = InjectionPointProvider.set(new InjectionPointImpl(injectionPointType, requiredType, + prev = InjectionPointProvider.setCurrent(context, new InjectionPointImpl(injectionPointType, requiredType, requiredQualifiers, targetBean, annotations, javaMember, position, isTransient)); } try { return bean.get(context); } finally { if (resetCurrentInjectionPoint) { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(context, prev); } } } @@ -317,7 +317,7 @@ private T getBeanInstance(InjectableBean bean) { CreationalContextImpl ctx = creationalContext.child(bean); InjectionPoint prev = null; if (resetCurrentInjectionPoint) { - prev = InjectionPointProvider.set(new InjectionPointImpl(injectionPointType, requiredType, + prev = InjectionPointProvider.setCurrent(ctx, new InjectionPointImpl(injectionPointType, requiredType, requiredQualifiers, targetBean, annotations, javaMember, position, isTransient)); } T instance; @@ -325,7 +325,7 @@ private T getBeanInstance(InjectableBean bean) { instance = bean.get(ctx); } finally { if (resetCurrentInjectionPoint) { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(ctx, prev); } } return instance; diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java index 66c7f7b1b8261..f65b917fd3f5d 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Instances.java @@ -67,15 +67,14 @@ public static List listOf(InjectableBean targetBean, Type injectionPoi return Collections.emptyList(); } List list = new ArrayList<>(beans.size()); - InjectionPoint prev = InjectionPointProvider - .set(new InjectionPointImpl(injectionPointType, requiredType, requiredQualifiers, targetBean, - annotations, javaMember, position, isTransient)); + InjectionPoint prev = InjectionPointProvider.setCurrent(creationalContext, new InjectionPointImpl(injectionPointType, + requiredType, requiredQualifiers, targetBean, annotations, javaMember, position, isTransient)); try { for (InjectableBean bean : beans) { list.add(getBeanInstance(CreationalContextImpl.unwrap(creationalContext), (InjectableBean) bean)); } } finally { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(creationalContext, prev); } return List.copyOf(list); @@ -126,12 +125,11 @@ private static InstanceHandle getHandle(CreationalContextImpl parent, @Override public T get() { - InjectionPoint prev = InjectionPointProvider - .set(injectionPointSupplier.get()); + InjectionPoint prev = InjectionPointProvider.setCurrent(ctx, injectionPointSupplier.get()); try { return bean.get(ctx); } finally { - InjectionPointProvider.set(prev); + InjectionPointProvider.setCurrent(ctx, prev); } } }, null); From 0d91bcbf63d4d3b886e578686160105a684504f2 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 12 Apr 2024 16:45:38 +0200 Subject: [PATCH 26/72] Fix the preview.yml workflow There were multiple problems: - download-artifact@v4 was unable to download the artifacts for whatever reason (it should work but it doesn't) - the size of the website was too large for surge. I applied the same tricks I applied to the website repo. --- .github/workflows/preview.yml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 25b59f46942e8..088caba664697 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -14,10 +14,22 @@ jobs: - uses: actions/checkout@v4 with: repository: quarkusio/quarkusio.github.io + fetch-depth: 5000 + fetch-tags: false + + - name: Install git-restore-time + run: sudo apt-get install -y git-restore-mtime + + - name: Restore mtime + run: git restore-mtime + + # There is a weird issue with download-artifact@v4 + # keeping the external action for now - name: Download PR Artifact - uses: actions/download-artifact@v4 + uses: dawidd6/action-download-artifact@v3 with: - run-id: ${{ github.event.workflow_run.workflow_id }} + workflow: ${{ github.event.workflow_run.workflow_id }} + workflow_conclusion: success name: documentation path: documentation-temp - name: Store PR id as variable @@ -72,6 +84,13 @@ jobs: custom_opts: '--config _config.yml,_only_latest_guides_config.yml' ### If you need to specify any Jekyll build options, enable the above input ### Flags accepted can be found here https://jekyllrb.com/docs/configuration/options/#build-command-options + + - name: Reduce the size of the website to be compatible with surge + run: | + find assets/images/posts/ -mindepth 1 -maxdepth 1 -type d -mtime +100 -exec rm -rf _site/{} \; + find newsletter/ -mindepth 1 -maxdepth 1 -type d -mtime +100 -exec rm -rf _site/{} \; + rm -rf _site/assets/images/worldtour/2023 + - name: Publishing to surge for preview id: deploy run: npx surge ./_site --domain https://quarkus-pr-main-${{ steps.pr.outputs.id }}-preview.surge.sh --token ${{ secrets.SURGE_TOKEN }} @@ -81,6 +100,10 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} body: | 🎊 PR Preview ${{ github.sha }} has been successfully built and deployed to https://quarkus-pr-main-${{ steps.pr.outputs.id }}-preview.surge.sh/version/main/guides/ + + - Images of blog posts older than 3 months are not available. + - Newsletters older than 3 months are not available. + body-include: '' From 3b57d0d67f6cce3efceeceb21336cacf0935d1fb Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 12 Apr 2024 16:46:47 +0200 Subject: [PATCH 27/72] Do not build main CI for doc workflow changes --- .github/workflows/ci-actions-incremental.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 75f690c897780..3faf0721fa9f3 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -18,6 +18,8 @@ on: - '.github/*.yml' - '.github/*.java' - '.github/*.conf' + - '.github/workflows/doc-build.yml' + - '.github/workflows/preview.yml' pull_request: types: [opened, synchronize, reopened, ready_for_review] paths-ignore: @@ -33,6 +35,8 @@ on: - '.github/*.yml' - '.github/*.java' - '.github/*.conf' + - '.github/workflows/doc-build.yml' + - '.github/workflows/preview.yml' workflow_dispatch: concurrency: From 33dc7b7c76293567da1a1f85412235227dc3f180 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 12 Apr 2024 17:02:29 +0200 Subject: [PATCH 28/72] Update Ruby version used for preview --- .github/workflows/preview.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 088caba664697..ba6d5146d4d6d 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -45,7 +45,7 @@ jobs: - name: Set up ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 # can change this to 2.7 or whatever version you prefer + ruby-version: 3.2.3 - name: Build Jekyll site uses: limjh16/jekyll-action-ts@v2 with: From 3d1b6dec1dcb15765a093fcd5d4248deb7eeae93 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 12 Apr 2024 17:15:32 +0200 Subject: [PATCH 29/72] Add topics to validation.adoc to test the preview --- docs/src/main/asciidoc/validation.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/validation.adoc b/docs/src/main/asciidoc/validation.adoc index 60bbc6be1eb68..425d97b01fc7b 100644 --- a/docs/src/main/asciidoc/validation.adoc +++ b/docs/src/main/asciidoc/validation.adoc @@ -7,7 +7,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :categories: web, data :summary: This guide covers how to use Hibernate Validator/Bean Validation in your REST services. -:topics: bean-validation,hibernate-validator,validation +:topics: bean-validation,hibernate-validator,validation,validator,constraints :extensions: io.quarkus:quarkus-hibernate-validator This guide covers how to use Hibernate Validator/Bean Validation for: From 7210f5d0ea4f78869ba7e510c19b0e726590f0b4 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 12 Apr 2024 18:32:09 +0300 Subject: [PATCH 30/72] Add a note about FileUpload and FileDownload These types should not have been added to the common module, they should have gone into the server part --- .../org/jboss/resteasy/reactive/multipart/FileDownload.java | 5 +++++ .../org/jboss/resteasy/reactive/multipart/FileUpload.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileDownload.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileDownload.java index 8a797e2766aee..25f48b6819786 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileDownload.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileDownload.java @@ -1,4 +1,9 @@ package org.jboss.resteasy.reactive.multipart; +/** + * Represent a file that should be pushed to the client. + *

+ * WARNING: This type is currently only supported on the server + */ public interface FileDownload extends FilePart { } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java index 7576168d51aa6..57a2bc9996ffa 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java @@ -2,6 +2,11 @@ import java.nio.file.Path; +/** + * Represent a file that has been uploaded. + *

+ * WARNING: This type is currently only supported on the server + */ public interface FileUpload extends FilePart { /** From 35f322fc8b1352e8ff9bc53fe54a0e7643985e02 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 12 Apr 2024 18:29:09 +0300 Subject: [PATCH 31/72] Improve the documentation about sending multipart with the REST Client Co-authored-by: Guillaume Smet --- docs/src/main/asciidoc/rest-client.adoc | 199 ++++++++++++------------ 1 file changed, 99 insertions(+), 100 deletions(-) diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index 549b2fd6fa886..4df2562e291fe 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -325,106 +325,6 @@ public interface ExtensionsService { } ---- - -=== Using ClientMultipartForm - -MultipartForm can be built using the Class `ClientMultipartForm` which supports building the form as needed: - -`ClientMultipartForm` can be programmatically created with custom inputs and/or from `MultipartFormDataInput` and/or from custom Quarkus REST Input annotated with `@RestForm` if received. - -[source, java] ----- -public interface MultipartService { - - @POST - @Path("/multipart") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @Produces(MediaType.APPLICATION_JSON) - Map multipart(ClientMultipartForm dataParts); // <1> -} ----- - -<1> input to the method is a custom Generic `ClientMultipartForm` which matches external application api contract. - - -More information about this Class and supported methods can be found on the javadoc of link:https://javadoc.io/doc/io.quarkus.resteasy.reactive/resteasy-reactive-client/latest/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.html[`ClientMultipartForm`]. - - -Build `ClientMultipartForm` from `MultipartFormDataInput` programmatically - -[source, java] ----- -public ClientMultipartForm buildClientMultipartForm(MultipartFormDataInput inputForm) // <1> - throws IOException { - ClientMultipartForm multiPartForm = ClientMultipartForm.create(); // <2> - for (Entry> attribute : inputForm.getValues().entrySet()) { - for (FormValue fv : attribute.getValue()) { - if (fv.isFileItem()) { - final FileItem fi = fv.getFileItem(); - String mediaType = Objects.toString(fv.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE), - MediaType.APPLICATION_OCTET_STREAM); - if (fi.isInMemory()) { - multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(), - Buffer.buffer(IOUtils.toByteArray(fi.getInputStream())), mediaType); // <3> - } else { - multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(), - fi.getFile().toString(), mediaType); // <4> - } - } else { - multiPartForm.attribute(attribute.getKey(), fv.getValue(), fv.getFileName()); // <5> - } - } - } - return multiPartForm; -} ----- - -<1> `MultipartFormDataInput` inputForm supported by Quarkus REST (Server). -<2> Creating a `ClientMultipartForm` object to populate with various dataparts. -<3> Adding InMemory `FileItem` to `ClientMultipartForm` -<4> Adding physical `FileItem` to `ClientMultipartForm` -<5> Adding any attribute directly to `ClientMultipartForm` if not `FileItem`. - -Build `ClientMultipartForm` from custom Attributes annotated with `@RestForm` - -[source, java] ----- -public class MultiPartPayloadFormData { // <1> - - @RestForm("files") - @PartType(MediaType.APPLICATION_OCTET_STREAM) - List files; - - @RestForm("jsonPayload") - @PartType(MediaType.TEXT_PLAIN) - String jsonPayload; -} - -/* - * Generate ClientMultipartForm from custom attributes annotated with @RestForm - */ -public ClientMultipartForm buildClientMultipartForm(MultiPartPayloadFormData inputForm) { // <1> - ClientMultipartForm multiPartForm = ClientMultipartForm.create(); - multiPartForm.attribute("jsonPayload", inputForm.getJsonPayload(), "jsonPayload"); // <2> - inputForm.getFiles().forEach(fu -> { - multiPartForm.binaryFileUpload("file", fu.name(), fu.filePath().toString(), fu.contentType()); // <3> - }); - return multiPartForm; -} ----- - -<1> `MultiPartPayloadFormData` custom Object created to match the API contract for calling service which needs to be converted to `ClientMultipartForm` -<2> Adding attribute `jsonPayload` directly to `ClientMultipartForm` -<3> Adding `FileUpload` objects to `ClientMultipartForm` as binaryFileUpload with contentType. - -[NOTE] -==== -When sending multipart data that uses the same name, problems can arise if the client and server do not use the same multipart encoder mode. -By default, the REST Client uses `RFC1738`, but depending on the situation, clients may need to be configured with `HTML5` or `RFC3986` mode. - -This configuration can be achieved via the `quarkus.rest-client.multipart-post-encoder-mode` property. -==== - === Sending large payloads The REST Client is capable of sending arbitrarily large HTTP bodies without buffering the contents in memory, if one of the following types is used: @@ -1554,6 +1454,105 @@ You can also send JSON multiparts by specifying the `@PartType` annotation: String sendMultipart(@RestForm @PartType(MediaType.APPLICATION_JSON) Person person); ---- +==== Programmatically creating the Multipart form + +In cases where the multipart content needs to be built up programmatically, the REST Client provides `ClientMultipartForm` which can be used in the REST Client like so: + +[source, java] +---- +public interface MultipartService { + + @POST + @Path("/multipart") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + Map multipart(ClientMultipartForm dataParts); +} +---- + + +More information about this class and supported methods can be found on the javadoc of link:https://javadoc.io/doc/io.quarkus.resteasy.reactive/resteasy-reactive-client/latest/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.html[`ClientMultipartForm`]. + +===== Converting a received multipart object into a client request + +A good example of creating `ClientMultipartForm` is one where it is created from the server's `MultipartFormDataInput` (which represents a multipart request received by xref:rest.adoc#multipart[Quarkus REST]) - the purpose being to propagate the request downstream while allowing for arbitrary modifications: + +[source, java] +---- +public ClientMultipartForm buildClientMultipartForm(MultipartFormDataInput inputForm) // <1> + throws IOException { + ClientMultipartForm multiPartForm = ClientMultipartForm.create(); // <2> + for (Entry> attribute : inputForm.getValues().entrySet()) { + for (FormValue fv : attribute.getValue()) { + if (fv.isFileItem()) { + final FileItem fi = fv.getFileItem(); + String mediaType = Objects.toString(fv.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE), + MediaType.APPLICATION_OCTET_STREAM); + if (fi.isInMemory()) { + multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(), + Buffer.buffer(IOUtils.toByteArray(fi.getInputStream())), mediaType); // <3> + } else { + multiPartForm.binaryFileUpload(attribute.getKey(), fv.getFileName(), + fi.getFile().toString(), mediaType); // <4> + } + } else { + multiPartForm.attribute(attribute.getKey(), fv.getValue(), fv.getFileName()); // <5> + } + } + } + return multiPartForm; +} +---- + +<1> `MultipartFormDataInput` is a Quarkus REST (Server) type representing a received multipart request. +<2> A `ClientMultipartForm` is created. +<3> `FileItem` attribute is created for the request attribute that represented an in memory file attribute +<4> `FileItem` attribute is created for the request attribute that represented a file attribute saved on the file system +<5> Non-file attributes added directly to `ClientMultipartForm` if not `FileItem`. + + +In a similar fashion if the received server multipart request is known and looks something like: + +[source, java] +---- +public class Request { // <1> + + @RestForm("files") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + List files; + + @RestForm("jsonPayload") + @PartType(MediaType.TEXT_PLAIN) + String jsonPayload; +} +---- + +the `ClientMultipartForm` can be created easily as follows: + +[source, java] +---- +public ClientMultipartForm buildClientMultipartForm(Request request) { // <1> + ClientMultipartForm multiPartForm = ClientMultipartForm.create(); + multiPartForm.attribute("jsonPayload", request.getJsonPayload(), "jsonPayload"); // <2> + request.getFiles().forEach(fu -> { + multiPartForm.binaryFileUpload("file", fu.name(), fu.filePath().toString(), fu.contentType()); // <3> + }); + return multiPartForm; +} +---- + +<1> `Request` representing the request the server parts accepts +<2> A `jsonPayload` attribute is added directly to `ClientMultipartForm` +<3> A `binaryFileUpload` is created from the request's `FileUpload` (which is a Quarkus REST (Server) type used to represent a binary file upload) + +[NOTE] +==== +When sending multipart data that uses the same name, problems can arise if the client and server do not use the same multipart encoder mode. +By default, the REST Client uses `RFC1738`, but depending on the situation, clients may need to be configured with `HTML5` or `RFC3986` mode. + +This configuration can be achieved via the `quarkus.rest-client.multipart-post-encoder-mode` property. +==== + === Receiving Multipart Messages REST Client also supports receiving multipart messages. As with sending, to parse a multipart response, you need to create a class that describes the response data, e.g. From 75ad6a55e50df92ef130e56032012f3f07c258f4 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 12 Apr 2024 19:03:13 +0200 Subject: [PATCH 32/72] Fix collapsing in config reference I'm not sure when it got broken but the current toggling is not working very well at least in some browsers. Using good old add/remove was able to fix it so I didn't dig too much either. --- docs/src/main/asciidoc/javascript/config.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/src/main/asciidoc/javascript/config.js b/docs/src/main/asciidoc/javascript/config.js index 8761d8f9c1fdc..e72b1c18a794e 100644 --- a/docs/src/main/asciidoc/javascript/config.js +++ b/docs/src/main/asciidoc/javascript/config.js @@ -270,13 +270,15 @@ function makeCollapsibleHandler(descDiv, td, row, if( isCollapsed ) { collapsibleSpan.childNodes.item(0).nodeValue = 'Show less'; iconDecoration.classList.replace('fa-chevron-down', 'fa-chevron-up'); + descDiv.classList.remove('description-collapsed'); + descDiv.classList.add('description-expanded'); } else { collapsibleSpan.childNodes.item(0).nodeValue = 'Show more'; iconDecoration.classList.replace('fa-chevron-up', 'fa-chevron-down'); + descDiv.classList.add('description-collapsed'); + descDiv.classList.remove('description-expanded'); } - descDiv.classList.toggle('description-collapsed'); - descDiv.classList.toggle('description-expanded'); row.classList.toggle('row-collapsed'); }; } From b4ae2ce43d2fa84f5782e8b40a7f3ea9b983ddaf Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 5 Apr 2024 14:21:55 +0200 Subject: [PATCH 33/72] Isolate testThatNewResourcesAreServed in FlakyDevMojoIT Also modernize things a bit now that we are using Java 17+. --- .../dev/RuntimeUpdatesProcessor.java | 27 ++- .../java/io/quarkus/maven/it/DevMojoIT.java | 33 --- .../io/quarkus/maven/it/FlakyDevMojoIT.java | 55 +++++ .../projects/classic-with-log/.env | 1 + .../projects/classic-with-log/pom.xml | 108 ++++++++++ .../java/org/acme/ClasspathResources.java | 195 ++++++++++++++++++ .../src/main/java/org/acme/HelloResource.java | 74 +++++++ .../src/main/java/org/acme/MyApplication.java | 9 + .../main/java/org/acme/ProtectionDomain.java | 77 +++++++ .../resources/META-INF/resources/index.html | 156 ++++++++++++++ .../src/main/resources/application.properties | 9 + .../src/main/resources/assets/test.txt | 0 .../src/main/resources/db/location/test.sql | 7 + .../test/java/org/acme/HelloResourceTest.java | 21 ++ 14 files changed, 730 insertions(+), 42 deletions(-) create mode 100644 integration-tests/maven/src/test/java/io/quarkus/maven/it/FlakyDevMojoIT.java create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/.env create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/pom.xml create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ClasspathResources.java create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/HelloResource.java create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/MyApplication.java create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ProtectionDomain.java create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/META-INF/resources/index.html create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/application.properties create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/assets/test.txt create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/db/location/test.sql create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/test/java/org/acme/HelloResourceTest.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java index 1b57c9aa9ab57..de09063b148bb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java @@ -18,6 +18,7 @@ import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.ArrayList; @@ -936,11 +937,14 @@ Set checkForFileChange(Function checkForFileChange(Function checkForFileChange(Function checkForFileChange(Function last) { // Use either the absolute path or the OS-agnostic path to match the HotDeploymentWatchedFileBuildItem ret.add(isAbsolute ? watchedPath.filePath.toString() : watchedPath.getOSAgnosticMatchPath()); @@ -1372,7 +1380,8 @@ private boolean isAbsolute() { @Override public String toString() { - return "WatchedPath [matchPath=" + matchPath + ", filePath=" + filePath + ", restartNeeded=" + restartNeeded + "]"; + return "WatchedPath [matchPath=" + matchPath + ", filePath=" + filePath + ", restartNeeded=" + restartNeeded + + ", lastModified=" + lastModified + "]"; } } diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java index 39fd504562a81..262bc40ceab09 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java @@ -3,8 +3,6 @@ import static io.quarkus.maven.it.ApplicationNameAndVersionTestUtil.assertApplicationPropertiesSetCorrectly; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -943,37 +941,6 @@ public void testThatExternalConfigOverridesConfigInJar() throws MavenInvocationE .until(() -> devModeClient.getHttpResponse("/app/hello/greeting").contains(uuid)); } - @Test - public void testThatNewResourcesAreServed() throws MavenInvocationException, IOException { - testDir = initProject("projects/classic", "projects/project-classic-run-resource-change"); - runAndCheck(); - - // Create a new resource - File source = new File(testDir, "src/main/resources/META-INF/resources/lorem.txt"); - FileUtils.write(source, - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", - "UTF-8"); - await() - .pollDelay(100, TimeUnit.MILLISECONDS) - .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) - .until(() -> devModeClient.getHttpResponse("/lorem.txt"), containsString("Lorem ipsum")); - - // Update the resource - String uuid = UUID.randomUUID().toString(); - FileUtils.write(source, uuid, "UTF-8"); - await() - .pollDelay(100, TimeUnit.MILLISECONDS) - .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) - .until(() -> devModeClient.getHttpResponse("/lorem.txt"), equalTo(uuid)); - - // Delete the resource - source.delete(); - await() - .pollDelay(100, TimeUnit.MILLISECONDS) - .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) - .until(() -> devModeClient.getHttpResponse("/lorem.txt", 404)); - } - @Test public void testThatConfigFileDeletionsAreDetected() throws MavenInvocationException, IOException { testDir = initProject("projects/dev-mode-file-deletion"); diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/FlakyDevMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/FlakyDevMojoIT.java new file mode 100644 index 0000000000000..a2b4beaacf714 --- /dev/null +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/FlakyDevMojoIT.java @@ -0,0 +1,55 @@ +package io.quarkus.maven.it; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.devmode.util.DevModeClient; + +/** + * This test has been isolated as it is very flaky and causing issues with Develocity PTS. + */ +@DisableForNative +public class FlakyDevMojoIT extends RunAndCheckMojoTestBase { + + protected DevModeClient devModeClient = new DevModeClient(getPort()); + + @Test + public void testThatNewResourcesAreServed() throws MavenInvocationException, IOException { + testDir = initProject("projects/classic-with-log", "projects/project-classic-run-resource-change"); + runAndCheck(); + + // Create a new resource + Path source = testDir.toPath().resolve("src/main/resources/META-INF/resources/lorem.txt"); + Files.writeString(source, + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."); + await() + .pollDelay(100, TimeUnit.MILLISECONDS) + .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) + .until(() -> devModeClient.getHttpResponse("/lorem.txt"), containsString("Lorem ipsum")); + + // Update the resource + String uuid = UUID.randomUUID().toString(); + Files.writeString(source, uuid); + await() + .pollDelay(100, TimeUnit.MILLISECONDS) + .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) + .until(() -> devModeClient.getHttpResponse("/lorem.txt"), equalTo(uuid)); + + // Delete the resource + Files.delete(source); + await() + .pollDelay(100, TimeUnit.MILLISECONDS) + .atMost(TestUtils.getDefaultTimeout(), TimeUnit.MINUTES) + .until(() -> devModeClient.getHttpResponse("/lorem.txt", 404)); + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/.env b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/.env new file mode 100644 index 0000000000000..98fb9ae1398c6 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/.env @@ -0,0 +1 @@ +OTHER_GREETING=Hola \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/pom.xml b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/pom.xml new file mode 100644 index 0000000000000..6e8f3dfe1026f --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + org.acme + acme + 1.0-SNAPSHOT + + io.quarkus + quarkus-bom + @project.version@ + @project.version@ + ${compiler-plugin.version} + UTF-8 + ${maven.compiler.source} + ${maven.compiler.target} + + + 1.13.0 + + + + + + \${quarkus.platform.group-id} + \${quarkus.platform.artifact-id} + \${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-smallrye-context-propagation + + + io.quarkus + quarkus-websockets + + + io.smallrye.common + smallrye-common-vertx-context + 1.13.2 + + + org.webjars + jquery-ui + \${webjar.jquery-ui.version} + + + commons-io + commons-io + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + maven-compiler-plugin + \${compiler-plugin.version} + + + io.quarkus + quarkus-maven-plugin + \${quarkus-plugin.version} + + + + generate-code + generate-code-tests + build + + + + + + + + + native + + true + + + + customOutputDir + + target-other + + + + diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ClasspathResources.java b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ClasspathResources.java new file mode 100644 index 0000000000000..a8a4efacded05 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ClasspathResources.java @@ -0,0 +1,195 @@ +package org.acme; + +import jakarta.ws.rs.QueryParam; +import org.apache.commons.io.IOUtils; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.function.Supplier; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +@Path("/classpathResources") +public class ClasspathResources { + + private static final String SUCCESS = "success"; + + @GET + public String readClassPathResources() { + return runAssertions( + () -> assertInvalidExactFileLocation(), + () -> assertCorrectExactFileLocation(), + () -> assertInvalidDirectory(), + () -> assertCorrectDirectory(), + () -> assertTopLevelDirectory(), + () -> assertMultiRelease() + ); + } + + private String runAssertions(Supplier... assertions) { + String result; + for (Supplier assertion : assertions) { + result = assertion.get(); + if (!SUCCESS.equals(result)) { + return result; + } + } + return SUCCESS; + } + + private String assertInvalidExactFileLocation() { + final String testType = "invalid-exact-location"; + try { + Enumeration exactFileLocationEnumeration = this.getClass().getClassLoader().getResources("db/location/test2.sql"); + List exactFileLocationList = urlList(exactFileLocationEnumeration); + if (exactFileLocationList.size() != 0) { + return errorResult(testType, "wrong number of urls"); + } + return SUCCESS; + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private String assertMultiRelease() { + final String testType = "assert-multi-release-jar"; + if (System.getProperty("java.version").startsWith("1.")) { + return SUCCESS; + } + try { + //this class is only present in multi release jars + //for fast-jar we need to make sure it is loaded correctly + Class clazz = this.getClass().getClassLoader().loadClass("io.smallrye.common.vertx.VertxContext"); + if (clazz.getClassLoader() == getClass().getClassLoader()) { + return SUCCESS; + } + return errorResult(testType, "Incorrect ClassLoader for " + clazz); + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + private String assertCorrectExactFileLocation() { + final String testType = "correct-exact-location"; + try { + Enumeration exactFileLocationEnumeration = this.getClass().getClassLoader().getResources("db/location/test.sql"); + List exactFileLocationList = urlList(exactFileLocationEnumeration); + if (exactFileLocationList.size() != 1) { + return errorResult(testType, "wrong number of urls"); + } + String fileContent = IOUtils.toString(exactFileLocationList.get(0).toURI(), StandardCharsets.UTF_8); + if (!fileContent.contains("CREATE TABLE")) { + return errorResult(testType, "wrong file content"); + } + return SUCCESS; + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private String assertInvalidDirectory() { + final String testType = "invalid-directory"; + try { + Enumeration exactFileLocationEnumeration = this.getClass().getClassLoader().getResources("db/location2"); + List exactFileLocationList = urlList(exactFileLocationEnumeration); + if (exactFileLocationList.size() != 0) { + return errorResult(testType, "wrong number of urls"); + } + return SUCCESS; + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private String assertCorrectDirectory() { + final String testType = "correct-directory"; + try { + Enumeration directoryEnumeration = this.getClass().getClassLoader().getResources("db/location"); + List directoryURLList = urlList(directoryEnumeration); + if (directoryURLList.size() != 1) { + return errorResult(testType, "wrong number of directory urls"); + } + + URL singleURL = directoryURLList.get(0); + + int separatorIndex = singleURL.getPath().lastIndexOf('!'); + String jarPath = singleURL.getPath().substring(0, separatorIndex); + String directoryName = singleURL.getPath().substring(separatorIndex + 2) + "/"; + + try (JarFile jarFile = new JarFile(Paths.get(new URI(jarPath)).toFile())) { + Enumeration entries = jarFile.entries(); + List entriesInDirectory = new ArrayList<>(); + while (entries.hasMoreElements()) { + JarEntry currentEntry = entries.nextElement(); + String entryName = currentEntry.getName(); + if (entryName.startsWith(directoryName) && !entryName.equals(directoryName)) { + entriesInDirectory.add(currentEntry); + } + } + + if (entriesInDirectory.size() != 1) { + return errorResult(testType, "wrong number of entries in jar directory"); + } + + try (InputStream is = jarFile.getInputStream(entriesInDirectory.get(0))) { + String fileContent = IOUtils.toString(is, StandardCharsets.UTF_8); + if (!fileContent.contains("CREATE TABLE")) { + return errorResult(testType, "wrong file content"); + } + return SUCCESS; + } + } + + + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private String assertTopLevelDirectory() { + final String testType = "top-level-directory"; + try { + Enumeration directoryEnumeration = this.getClass().getClassLoader().getResources("assets"); + List directoryURLList = urlList(directoryEnumeration); + if (directoryURLList.size() != 1) { + return errorResult(testType, "wrong number of directory urls"); + } + + return SUCCESS; + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private List urlList(Enumeration enumeration) { + if (enumeration == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + while (enumeration.hasMoreElements()) { + result.add(enumeration.nextElement()); + } + return result; + } + + private String errorResult(String testType, String message) { + return testType + " / " + message; + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/HelloResource.java b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/HelloResource.java new file mode 100644 index 0000000000000..c21e5305ea793 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/HelloResource.java @@ -0,0 +1,74 @@ +package org.acme; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class HelloResource { + + @ConfigProperty(name = "greeting") + String greeting; + + @ConfigProperty(name = "quarkus.application.version") + String applicationVersion; + + @ConfigProperty(name = "quarkus.application.name") + String applicationName; + + @ConfigProperty(name = "other.greeting", defaultValue = "other") + String otherGreeting; + + @ConfigProperty(name = "quarkus.profile") + String profile; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "hello"; + } + + @GET + @Path("/greeting") + @Produces(MediaType.TEXT_PLAIN) + public String greeting() { + return greeting; + } + + @GET + @Path("/package") + @Produces(MediaType.TEXT_PLAIN) + public String pkg() { + return Blah.class.getPackage().getName(); + } + + @GET + @Path("/nameAndVersion") + @Produces(MediaType.TEXT_PLAIN) + public String nameAndVersion() { + return applicationName + "/" + applicationVersion; + } + + @GET + @Path("/otherGreeting") + @Produces(MediaType.TEXT_PLAIN) + public String otherGreeting() { + return otherGreeting; + } + + @GET + @Path("/profile") + @Produces(MediaType.TEXT_PLAIN) + public String profile() { + return profile; + } + + + public static class Blah { + + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/MyApplication.java b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/MyApplication.java new file mode 100644 index 0000000000000..a6d66f8b9eda2 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/MyApplication.java @@ -0,0 +1,9 @@ +package org.acme; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +@ApplicationPath("/app") +public class MyApplication extends Application { + +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ProtectionDomain.java b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ProtectionDomain.java new file mode 100644 index 0000000000000..36cfbe3d82e5e --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/java/org/acme/ProtectionDomain.java @@ -0,0 +1,77 @@ +package org.acme; + +import org.apache.commons.io.IOUtils; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.function.Supplier; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; + +@Path("/protectionDomain") +public class ProtectionDomain { + + private static final String SUCCESS = "success"; + + @GET + public String useProtectionDomain() { + return runAssertions( + () -> assertReadManifestFromJar() + ); + } + + private String runAssertions(Supplier... assertions) { + String result; + for (Supplier assertion : assertions) { + result = assertion.get(); + if (!SUCCESS.equals(result)) { + return result; + } + } + return SUCCESS; + } + + private String assertReadManifestFromJar() { + final String testType = "manifest-from-jar"; + try { + URL location = org.apache.commons.io.Charsets.class.getProtectionDomain().getCodeSource().getLocation(); + if (location == null) { + return errorResult(testType, "location should not be null"); + } + + try (InputStream inputStream = location.openStream()) { + try (JarInputStream jarInputStream = new JarInputStream(inputStream)) { + Manifest manifest = jarInputStream.getManifest(); + if (manifest == null) { + return errorResult(testType, "manifest should not be null"); + } + String implementationVersion = manifest.getMainAttributes().getValue("Implementation-Version"); + if (implementationVersion == null) { + return errorResult(testType, "implementation-version should not be null"); + } + } + } + return SUCCESS; + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + + private String errorResult(String testType, String message) { + return testType + " / " + message; + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/META-INF/resources/index.html b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000000000..c09bb5c96b869 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,156 @@ + + + + + acme - 1.0-SNAPSHOT + + + + +

+ +
+
+

Congratulations, you have created a new Quarkus application.

+ +

Why do you see this?

+ +

This page is served by Quarkus. The source is in + src/main/resources/META-INF/resources/index.html.

+ +

What can I do from here?

+ +

If not already done, run the application in dev mode using: mvn compile quarkus:dev. +

+
    +
  • Add REST resources, Servlets, functions and other services in src/main/java.
  • +
  • Your static assets are located in src/main/resources/META-INF/resources.
  • +
  • Configure your application in src/main/resources/application.properties. +
  • +
+ +

Do you like Quarkus?

+

Go give it a star on GitHub.

+ +

How do I get rid of this page?

+

Just delete the src/main/resources/META-INF/resources/index.html file.

+
+
+
+

Application

+
    +
  • GroupId: org.acme
  • +
  • ArtifactId: acme
  • +
  • Version: 1.0-SNAPSHOT
  • +
  • Quarkus Version: 999-SNAPSHOT
  • +
+
+
+

Next steps

+ +
+
+
+ + + + \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/application.properties b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/application.properties new file mode 100644 index 0000000000000..4afcfeef83dca --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/application.properties @@ -0,0 +1,9 @@ +# Configuration file +key = value +greeting=bonjour +quarkus.log.level=INFO +quarkus.log.file.enable=false +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +quarkus.log.file.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %h %N[%i] %-5p [%c{3.}] (%t) %s%e%n +quarkus.log.category."io.quarkus".level=INFO +quarkus.log.category."io.quarkus.deployment.dev".level=DEBUG diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/assets/test.txt b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/assets/test.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/db/location/test.sql b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/db/location/test.sql new file mode 100644 index 0000000000000..cddc725179c67 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/main/resources/db/location/test.sql @@ -0,0 +1,7 @@ +CREATE TABLE TEST_SCHEMA.quarkus_table2 +( + id INT, + name VARCHAR(20) +); +INSERT INTO TEST_SCHEMA.quarkus_table2(id, name) +VALUES (1, '1.0.1 QUARKED'); diff --git a/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/test/java/org/acme/HelloResourceTest.java b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/test/java/org/acme/HelloResourceTest.java new file mode 100644 index 0000000000000..c2f29e2c9f711 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/classic-with-log/src/test/java/org/acme/HelloResourceTest.java @@ -0,0 +1,21 @@ +package org.acme; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +public class HelloResourceTest { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/app/hello") + .then() + .statusCode(200) + .body(is("hello")); + } + +} From 0879d5ec015b727fd1b6707e5875f0a5a253b8ea Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Sat, 13 Apr 2024 14:42:37 +0300 Subject: [PATCH 34/72] Add matching config issues based on title --- .github/quarkus-github-bot.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/quarkus-github-bot.yml b/.github/quarkus-github-bot.yml index 230db324a202f..06178aae8af14 100644 --- a/.github/quarkus-github-bot.yml +++ b/.github/quarkus-github-bot.yml @@ -474,6 +474,7 @@ triage: notify: [ebullient] - id: config labels: [area/config] + title: "config" directories: - extensions/config-yaml/ - core/deployment/src/main/java/io/quarkus/deployment/configuration/ From 4f6c9b8e4d0d966b9dbad4ef0d468399ccb45e56 Mon Sep 17 00:00:00 2001 From: Yoshikazu Nojima Date: Sun, 14 Apr 2024 18:31:39 +0900 Subject: [PATCH 35/72] Fix typo in cassandra.adoc --- docs/src/main/asciidoc/cassandra.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/cassandra.adoc b/docs/src/main/asciidoc/cassandra.adoc index 4168a3d906050..42cb2c8ba61f0 100644 --- a/docs/src/main/asciidoc/cassandra.adoc +++ b/docs/src/main/asciidoc/cassandra.adoc @@ -317,7 +317,7 @@ public class FruitDto { The translation to and from JSON is done automatically by the Quarkus REST (formerly RESTEasy Reactive) extension, which is included in this guide's pom.xml file. If you want to add it manually to your application, add the -below snippet to your application's ppm.xml file: +below snippet to your application's pom.xml file: [source,xml] ---- From 8afd81c9c553801ca9bc8a0271559d4e1273f66f Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Fri, 12 Apr 2024 20:15:21 +0100 Subject: [PATCH 36/72] Add OIDC TokenCertificateValidator --- ...rity-oidc-bearer-token-authentication.adoc | 60 ++++++++++++++++ .../oidc/TokenCertificateValidator.java | 25 +++++++ .../runtime/CertChainPublicKeyResolver.java | 68 +++++++++++++------ .../DynamicVerificationKeyResolver.java | 2 +- .../io/quarkus/oidc/runtime/OidcProvider.java | 59 ++++------------ ...erFinder.java => TenantFeatureFinder.java} | 30 +++++++- .../io/quarkus/it/keycloak/AdminResource.java | 8 +++ .../BearerGlobalTokenChainValidator.java | 29 ++++++++ .../BearerTenantTokenChainValidator.java | 34 ++++++++++ .../src/main/resources/application.properties | 3 + .../BearerTokenAuthorizationTest.java | 54 ++++++++++++++- .../io/quarkus/it/keycloak/TestUtils.java | 2 + 12 files changed, 302 insertions(+), 72 deletions(-) create mode 100644 extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenCertificateValidator.java rename extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/{TokenCustomizerFinder.java => TenantFeatureFinder.java} (53%) create mode 100644 integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerGlobalTokenChainValidator.java create mode 100644 integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerTenantTokenChainValidator.java diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc index 2a02b3292ca03..4d764c915a9e9 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -470,6 +470,66 @@ quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect For information about bearer access token propagation to the downstream services, see the xref:security-openid-connect-client-reference.adoc#token-propagation[Token propagation] section of the Quarkus "OpenID Connect (OIDC) and OAuth2 client and filters reference" guide. +=== JWT token certificate chain + +In some cases, JWT bearer tokens have an `x5c` header which represents an X509 certificate chain whose leaf certificate contains a public key that must be used to verify this token's signature. +Before this public key can be accepted to verify the signature, the certificate chain must be validated first. +The certificate chain validation involves several steps: + +1. Confirm that every certificate but the root one is signed by the parent certificate. + +2. Confirm the chain's root certificate is also imported in the truststore. + +3. Validate the chain's leaf certificate. If a common name of the leaf certificate is configured then a common name of the chain's leaf certificate must match it. Otherwise the chain's leaf certificate must also be avaiable in the truststore, unless one or more custom `TokenCertificateValidator` implementations are registered. + +4. `quarkus.oidc.TokenCertificateValidator` can be used to add a custom certificate chain validation step. It can be used by all tenants expecting tokens with the certificate chain or bound to specific OIDC tenants with the `@quarkus.oidc.TenantFeature` annotation. + +For example, here is how you can configure Quarkus OIDC to verify the token's certificate chain, without using `quarkus.oidc.TokenCertificateValidator`: + +[source,properties] +---- +quarkus.oidc.certificate-chain.trust-store-file=truststore-rootcert.p12 <1> +quarkus.oidc.certificate-chain.trust-store-password=storepassword +quarkus.oidc.certificate-chain.leaf-certificate-name=www.quarkusio.com <2> +---- +<1> The truststore must contain the certificate chain's root certificate. +<2> The certificate chain's leaf certificate must have a common name equal to `www.quarkusio.com`. If this property is not configured then the truststore must contain the certificate chain's leaf certificate unless one or more custom `TokenCertificateValidator` implementations are registered. + +You can add a custom certificate chain validation step by registering a custom `quarkus.oidc.TokenCertificateValidator`, for example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TokenCertificateValidator; +import io.quarkus.oidc.runtime.TrustStoreUtils; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +@Unremovable +public class BearerGlobalTokenChainValidator implements TokenCertificateValidator { + + @Override + public void validate(OidcTenantConfig oidcConfig, List chain, String tokenClaims) throws CertificateException { + String rootCertificateThumbprint = TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1)); + JsonObject claims = new JsonObject(tokenClaims); + if (!rootCertificateThumbprint.equals(claims.getString("root-certificate-thumbprint"))) { <1> + throw new CertificateException("Invalid root certificate"); + } + } +} + +---- +<1> Confirm that the certificate chain's root certificate is bound to the custom JWT token's claim. + === OIDC provider client authentication `quarkus.oidc.runtime.OidcProviderClient` is used when a remote request to an OIDC provider is required. diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenCertificateValidator.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenCertificateValidator.java new file mode 100644 index 0000000000000..f6914aedfd2cc --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenCertificateValidator.java @@ -0,0 +1,25 @@ +package io.quarkus.oidc; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * TokenCertificateValidator can be used to verify X509 certificate chain + * that is inlined in the JWT token as a 'x5c' header value. + * + * Use {@link TenantFeature} qualifier to bind this validator to specific OIDC tenants. + */ +public interface TokenCertificateValidator { + /** + * Validate X509 certificate chain + * + * @param oidcConfig current OIDC tenant configuration. + * @param chain the certificate chain. The first element in the list is a leaf certificate, the last element - the root + * certificate. + * @param tokenClaims the decoded JWT token claims in JSON format. If necessary, implementations can convert it to JSON + * object. + * @throws {@link CertificateException} if the certificate chain validation has failed. + */ + void validate(OidcTenantConfig oidcConfig, List chain, String tokenClaims) throws CertificateException; +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CertChainPublicKeyResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CertChainPublicKeyResolver.java index 133be33ae688c..d8d1999f0d20f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CertChainPublicKeyResolver.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CertChainPublicKeyResolver.java @@ -11,24 +11,32 @@ import org.jose4j.jwx.JsonWebStructure; import org.jose4j.lang.UnresolvableKeyException; -import io.quarkus.oidc.OidcTenantConfig.CertificateChain; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TokenCertificateValidator; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.runtime.X509IdentityProvider; import io.vertx.ext.auth.impl.CertificateHelper; public class CertChainPublicKeyResolver implements RefreshableVerificationKeyResolver { private static final Logger LOG = Logger.getLogger(OidcProvider.class); + final OidcTenantConfig oidcConfig; final Set thumbprints; final Optional expectedLeafCertificateName; + final List certificateValidators; - public CertChainPublicKeyResolver(CertificateChain chain) { - if (chain.getTrustStorePassword().isEmpty()) { + public CertChainPublicKeyResolver(OidcTenantConfig oidcConfig) { + this.oidcConfig = oidcConfig; + if (oidcConfig.certificateChain.getTrustStorePassword().isEmpty()) { throw new ConfigurationException( "Truststore with configured password which keeps thumbprints of the trusted certificates must be present"); } - this.thumbprints = TrustStoreUtils.getTrustedCertificateThumbprints(chain.trustStoreFile.get(), - chain.getTrustStorePassword().get(), chain.trustStoreCertAlias, chain.getTrustStoreFileType()); - this.expectedLeafCertificateName = chain.leafCertificateName; + this.thumbprints = TrustStoreUtils.getTrustedCertificateThumbprints( + oidcConfig.certificateChain.trustStoreFile.get(), + oidcConfig.certificateChain.getTrustStorePassword().get(), + oidcConfig.certificateChain.trustStoreCertAlias, + oidcConfig.certificateChain.getTrustStoreFileType()); + this.expectedLeafCertificateName = oidcConfig.certificateChain.leafCertificateName; + this.certificateValidators = TenantFeatureFinder.find(oidcConfig, TokenCertificateValidator.class); } @Override @@ -45,34 +53,52 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex LOG.debug("Token 'x5c' certificate chain is empty"); return null; } + + // General certificate chain validation + //TODO: support revocation lists + CertificateHelper.checkValidity(chain, null); + if (chain.size() == 1) { + // CertificateHelper.checkValidity does not currently + // verify the certificate signature if it is a single certificate chain + final X509Certificate root = chain.get(0); + root.verify(root.getPublicKey()); + } + + // Always do the root certificate thumbprint check LOG.debug("Checking a thumbprint of the root chain certificate"); String rootThumbprint = TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1)); if (!thumbprints.contains(rootThumbprint)) { LOG.error("Thumprint of the root chain certificate is invalid"); throw new UnresolvableKeyException("Thumprint of the root chain certificate is invalid"); } - if (expectedLeafCertificateName.isEmpty()) { - LOG.debug("Checking a thumbprint of the leaf chain certificate"); - String thumbprint = TrustStoreUtils.calculateThumprint(chain.get(0)); - if (!thumbprints.contains(thumbprint)) { - LOG.error("Thumprint of the leaf chain certificate is invalid"); - throw new UnresolvableKeyException("Thumprint of the leaf chain certificate is invalid"); + + // Run custom validators if any + if (!certificateValidators.isEmpty()) { + LOG.debug("Running custom TokenCertificateValidators"); + for (TokenCertificateValidator validator : certificateValidators) { + validator.validate(oidcConfig, chain, jws.getUnverifiedPayload()); } - } else { + } + + // Finally, check the leaf certificate if required + if (!expectedLeafCertificateName.isEmpty()) { + // Compare the leaf certificate common name against the configured value String leafCertificateName = X509IdentityProvider.getCommonName(chain.get(0).getSubjectX500Principal()); if (!expectedLeafCertificateName.get().equals(leafCertificateName)) { LOG.errorf("Wrong leaf certificate common name: %s", leafCertificateName); throw new UnresolvableKeyException("Wrong leaf certificate common name"); } + } else if (certificateValidators.isEmpty()) { + // No custom validators are registered and no leaf certificate CN is configured + // Check that the truststore contains a leaf certificate thumbprint + LOG.debug("Checking a thumbprint of the leaf chain certificate"); + String thumbprint = TrustStoreUtils.calculateThumprint(chain.get(0)); + if (!thumbprints.contains(thumbprint)) { + LOG.error("Thumprint of the leaf chain certificate is invalid"); + throw new UnresolvableKeyException("Thumprint of the leaf chain certificate is invalid"); + } } - //TODO: support revocation lists - CertificateHelper.checkValidity(chain, null); - if (chain.size() == 1) { - // CertificateHelper.checkValidity does not currently - // verify the certificate signature if it is a single certificate chain - final X509Certificate root = chain.get(0); - root.verify(root.getPublicKey()); - } + return chain.get(0).getPublicKey(); } catch (UnresolvableKeyException ex) { throw ex; diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java index dbb2adeb2af49..a2a2d85a2ab96 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DynamicVerificationKeyResolver.java @@ -39,7 +39,7 @@ public DynamicVerificationKeyResolver(OidcProviderClient client, OidcTenantConfi this.cache = new MemoryCache(client.getVertx(), config.jwks.cleanUpTimerInterval, config.jwks.cacheTimeToLive, config.jwks.cacheSize); if (config.certificateChain.trustStoreFile.isPresent()) { - chainResolverFallback = new CertChainPublicKeyResolver(config.certificateChain); + chainResolverFallback = new CertChainPublicKeyResolver(config); } else { chainResolverFallback = null; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index 54c96cbff24e1..a3a826541673c 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java @@ -4,14 +4,12 @@ import java.nio.charset.StandardCharsets; import java.security.Key; import java.time.Duration; -import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; -import jakarta.enterprise.inject.Default; import jakarta.json.JsonObject; import org.eclipse.microprofile.jwt.Claims; @@ -32,14 +30,11 @@ import org.jose4j.lang.InvalidAlgorithmException; import org.jose4j.lang.UnresolvableKeyException; -import io.quarkus.arc.Arc; import io.quarkus.logging.Log; import io.quarkus.oidc.AuthorizationCodeTokens; import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcConfigurationMetadata; import io.quarkus.oidc.OidcTenantConfig; -import io.quarkus.oidc.OidcTenantConfig.CertificateChain; -import io.quarkus.oidc.TenantFeature.TenantFeatureLiteral; import io.quarkus.oidc.TokenCustomizer; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; @@ -84,8 +79,8 @@ public class OidcProvider implements Closeable { final AlgorithmConstraints requiredAlgorithmConstraints; public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, Key tokenDecryptionKey) { - this(client, oidcConfig, jwks, TokenCustomizerFinder.find(oidcConfig), tokenDecryptionKey, - getCustomValidators(oidcConfig)); + this(client, oidcConfig, jwks, TenantFeatureFinder.find(oidcConfig), tokenDecryptionKey, + TenantFeatureFinder.find(oidcConfig, Validator.class)); } public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, @@ -94,10 +89,9 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json this.oidcConfig = oidcConfig; this.tokenCustomizer = tokenCustomizer; if (jwks != null) { - this.asymmetricKeyResolver = new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval, - oidcConfig.certificateChain); + this.asymmetricKeyResolver = new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval); } else if (oidcConfig != null && oidcConfig.certificateChain.trustStoreFile.isPresent()) { - this.asymmetricKeyResolver = new CertChainPublicKeyResolver(oidcConfig.certificateChain); + this.asymmetricKeyResolver = new CertChainPublicKeyResolver(oidcConfig); } else { this.asymmetricKeyResolver = null; } @@ -112,22 +106,17 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json this.requiredClaims = checkRequiredClaimsProp(); this.tokenDecryptionKey = tokenDecryptionKey; this.requiredAlgorithmConstraints = checkSignatureAlgorithm(); - - if (customValidators != null && !customValidators.isEmpty()) { - this.customValidators = customValidators; - } else { - this.customValidators = null; - } + this.customValidators = customValidators == null ? List.of() : customValidators; } public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenDecryptionKey) { this.client = null; this.oidcConfig = oidcConfig; - this.tokenCustomizer = TokenCustomizerFinder.find(oidcConfig); + this.tokenCustomizer = TenantFeatureFinder.find(oidcConfig); if (publicKeyEnc != null) { this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc); } else if (oidcConfig.certificateChain.trustStoreFile.isPresent()) { - this.asymmetricKeyResolver = new CertChainPublicKeyResolver(oidcConfig.certificateChain); + this.asymmetricKeyResolver = new CertChainPublicKeyResolver(oidcConfig); } else { throw new IllegalStateException("Neither public key nor certificate chain verification modes are enabled"); } @@ -137,7 +126,7 @@ public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenD this.requiredClaims = checkRequiredClaimsProp(); this.tokenDecryptionKey = tokenDecryptionKey; this.requiredAlgorithmConstraints = checkSignatureAlgorithm(); - this.customValidators = getCustomValidators(oidcConfig); + this.customValidators = TenantFeatureFinder.find(oidcConfig, Validator.class); } private AlgorithmConstraints checkSignatureAlgorithm() { @@ -223,10 +212,8 @@ private TokenVerificationResult verifyJwtTokenInternal(String token, builder.registerValidator(new CustomClaimsValidator(Map.of(OidcConstants.NONCE, nonce))); } - if (customValidators != null) { - for (Validator customValidator : customValidators) { - builder.registerValidator(customValidator); - } + for (Validator customValidator : customValidators) { + builder.registerValidator(customValidator); } if (issuedAtRequired) { @@ -438,11 +425,11 @@ private class JsonWebKeyResolver implements RefreshableVerificationKeyResolver { volatile long forcedJwksRefreshIntervalMilliSecs; final CertChainPublicKeyResolver chainResolverFallback; - JsonWebKeyResolver(JsonWebKeySet jwks, Duration forcedJwksRefreshInterval, CertificateChain chain) { + JsonWebKeyResolver(JsonWebKeySet jwks, Duration forcedJwksRefreshInterval) { this.jwks = jwks; this.forcedJwksRefreshIntervalMilliSecs = forcedJwksRefreshInterval.toMillis(); - if (chain.trustStoreFile.isPresent()) { - chainResolverFallback = new CertChainPublicKeyResolver(chain); + if (oidcConfig.certificateChain.trustStoreFile.isPresent()) { + chainResolverFallback = new CertChainPublicKeyResolver(oidcConfig); } else { chainResolverFallback = null; } @@ -618,24 +605,4 @@ public String validate(JwtContext jwtContext) throws MalformedClaimException { } } - private static List getCustomValidators(OidcTenantConfig oidcTenantConfig) { - if (oidcTenantConfig != null && oidcTenantConfig.tenantId.isPresent()) { - var tenantsValidators = new ArrayList(); - for (var instance : Arc.container().listAll(Validator.class, Default.Literal.INSTANCE)) { - if (instance.isAvailable()) { - tenantsValidators.add(instance.get()); - } - } - for (var instance : Arc.container().listAll(Validator.class, - TenantFeatureLiteral.of(oidcTenantConfig.tenantId.get()))) { - if (instance.isAvailable()) { - tenantsValidators.add(instance.get()); - } - } - if (!tenantsValidators.isEmpty()) { - return List.copyOf(tenantsValidators); - } - } - return null; - } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TokenCustomizerFinder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantFeatureFinder.java similarity index 53% rename from extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TokenCustomizerFinder.java rename to extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantFeatureFinder.java index d09633054b5fa..11a918f6dd5ba 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TokenCustomizerFinder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantFeatureFinder.java @@ -1,16 +1,22 @@ package io.quarkus.oidc.runtime; +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.inject.Default; + import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.TenantFeature.TenantFeatureLiteral; import io.quarkus.oidc.TokenCustomizer; -public class TokenCustomizerFinder { +public class TenantFeatureFinder { - private TokenCustomizerFinder() { + private TenantFeatureFinder() { } @@ -37,4 +43,24 @@ public static TokenCustomizer find(OidcTenantConfig oidcConfig) { return null; } + public static List find(OidcTenantConfig oidcTenantConfig, Class tenantFeatureClass) { + if (oidcTenantConfig != null && oidcTenantConfig.tenantId.isPresent()) { + var tenantsValidators = new ArrayList(); + for (var instance : Arc.container().listAll(tenantFeatureClass, Default.Literal.INSTANCE)) { + if (instance.isAvailable()) { + tenantsValidators.add(instance.get()); + } + } + for (var instance : Arc.container().listAll(tenantFeatureClass, + TenantFeatureLiteral.of(oidcTenantConfig.tenantId.get()))) { + if (instance.isAvailable()) { + tenantsValidators.add(instance.get()); + } + } + if (!tenantsValidators.isEmpty()) { + return List.copyOf(tenantsValidators); + } + } + return List.of(); + } } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java index 59664d4e21510..866a6d9ea0a40 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/AdminResource.java @@ -61,6 +61,14 @@ public String bearerCertificateFullChain() { return "granted:" + identity.getRoles(); } + @Path("bearer-chain-custom-validator") + @GET + @RolesAllowed("admin") + @Produces(MediaType.APPLICATION_JSON) + public String bearerCertificateCustomValidator() { + return "granted:" + identity.getRoles(); + } + @Path("bearer-certificate-full-chain-root-only") @GET @RolesAllowed("admin") diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerGlobalTokenChainValidator.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerGlobalTokenChainValidator.java new file mode 100644 index 0000000000000..d7e3589470420 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerGlobalTokenChainValidator.java @@ -0,0 +1,29 @@ +package io.quarkus.it.keycloak; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TokenCertificateValidator; +import io.quarkus.oidc.runtime.TrustStoreUtils; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +@Unremovable +public class BearerGlobalTokenChainValidator implements TokenCertificateValidator { + + @Override + public void validate(OidcTenantConfig oidcConfig, List chain, String tokenClaims) + throws CertificateException { + String rootCertificateThumbprint = TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1)); + JsonObject claims = new JsonObject(tokenClaims); + if (!rootCertificateThumbprint.equals(claims.getString("root-certificate-thumbprint"))) { + throw new CertificateException("Invalid root certificate"); + } + } + +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerTenantTokenChainValidator.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerTenantTokenChainValidator.java new file mode 100644 index 0000000000000..39a1ce4c06837 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/BearerTenantTokenChainValidator.java @@ -0,0 +1,34 @@ +package io.quarkus.it.keycloak; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.TokenCertificateValidator; +import io.quarkus.oidc.runtime.TrustStoreUtils; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +@Unremovable +@TenantFeature("bearer-chain-custom-validator") +public class BearerTenantTokenChainValidator implements TokenCertificateValidator { + + @Override + public void validate(OidcTenantConfig oidcConfig, List chain, String tokenClaims) + throws CertificateException { + if (!"bearer-chain-custom-validator".equals(oidcConfig.tenantId.get())) { + throw new RuntimeException("Unexpected tenant id"); + } + String leafCertificateThumbprint = TrustStoreUtils.calculateThumprint(chain.get(0)); + JsonObject claims = new JsonObject(tokenClaims); + if (!leafCertificateThumbprint.equals(claims.getString("leaf-certificate-thumbprint"))) { + throw new CertificateException("Invalid leaf certificate"); + } + } + +} diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index 807d619906036..15e351b94c6bf 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -196,6 +196,9 @@ quarkus.oidc.bearer-no-introspection.token.allow-jwt-introspection=false quarkus.oidc.bearer-certificate-full-chain.certificate-chain.trust-store-file=truststore.p12 quarkus.oidc.bearer-certificate-full-chain.certificate-chain.trust-store-password=storepassword +quarkus.oidc.bearer-chain-custom-validator.certificate-chain.trust-store-file=truststore.p12 +quarkus.oidc.bearer-chain-custom-validator.certificate-chain.trust-store-password=storepassword + quarkus.oidc.bearer-certificate-full-chain-root-only-wrongcname.certificate-chain.trust-store-file=truststore-rootcert.p12 quarkus.oidc.bearer-certificate-full-chain-root-only-wrongcname.certificate-chain.trust-store-password=storepassword quarkus.oidc.bearer-certificate-full-chain-root-only-wrongcname.certificate-chain.leaf-certificate-name=www.quarkusio.com diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index d95361d301e6c..af9862304184f 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -29,6 +29,7 @@ import io.quarkus.deployment.util.FileUtil; import io.quarkus.oidc.runtime.OidcUtils; +import io.quarkus.oidc.runtime.TrustStoreUtils; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.oidc.server.OidcWireMock; @@ -36,6 +37,7 @@ import io.restassured.RestAssured; import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.build.JwtClaimsBuilder; import io.smallrye.jwt.util.KeyUtils; import io.smallrye.jwt.util.ResourceUtils; import io.vertx.core.json.JsonObject; @@ -187,13 +189,44 @@ public void testAccessAdminResourceWithWrongCertS256Thumbprint() { .statusCode(401); } + @Test + public void testCertChainWithCustomValidator() throws Exception { + X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); + X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); + X509Certificate subjectCert = KeyUtils.getCertificate(ResourceUtils.readResource("/www.quarkustest.com.cert.pem")); + PrivateKey subjectPrivateKey = KeyUtils.readPrivateKey("/www.quarkustest.com.key.pem"); + + // Send the token with the valid certificate chain and bind it to the token claim + String accessToken = getAccessTokenForCustomValidator( + List.of(subjectCert, intermediateCert, rootCert), + subjectPrivateKey, true); + + RestAssured.given().auth().oauth2(accessToken) + .when().get("/api/admin/bearer-chain-custom-validator") + .then() + .statusCode(200) + .body(Matchers.containsString("admin")); + + // Send the token with the valid certificate chain but do bind it to the token claim + accessToken = getAccessTokenForCustomValidator( + List.of(subjectCert, intermediateCert, rootCert), + subjectPrivateKey, false); + + RestAssured.given().auth().oauth2(accessToken) + .when().get("/api/admin/bearer-chain-custom-validator") + .then() + .statusCode(401); + + } + @Test public void testAccessAdminResourceWithFullCertChain() throws Exception { X509Certificate rootCert = KeyUtils.getCertificate(ResourceUtils.readResource("/ca.cert.pem")); X509Certificate intermediateCert = KeyUtils.getCertificate(ResourceUtils.readResource("/intermediate.cert.pem")); X509Certificate subjectCert = KeyUtils.getCertificate(ResourceUtils.readResource("/www.quarkustest.com.cert.pem")); PrivateKey subjectPrivateKey = KeyUtils.readPrivateKey("/www.quarkustest.com.key.pem"); - // Send the token with the valid certificate chain + + // Send the token with the valid certificate chain and bind it to the token claim String accessToken = getAccessTokenWithCertChain( List.of(subjectCert, intermediateCert, rootCert), subjectPrivateKey); @@ -708,7 +741,24 @@ private String getAccessTokenWithCertChain(List chain, .groups("admin") .issuer("https://server.example.com") .audience("https://service.example.com") - .jws().chain(chain) + .claim("root-certificate-thumbprint", TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1))) + .jws() + .chain(chain) + .sign(privateKey); + } + + private String getAccessTokenForCustomValidator(List chain, + PrivateKey privateKey, boolean setLeafCertThumbprint) throws Exception { + JwtClaimsBuilder builder = Jwt.preferredUserName("alice") + .groups("admin") + .issuer("https://server.example.com") + .audience("https://service.example.com") + .claim("root-certificate-thumbprint", TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1))); + if (setLeafCertThumbprint) { + builder.claim("leaf-certificate-thumbprint", TrustStoreUtils.calculateThumprint(chain.get(0))); + } + return builder.jws() + .chain(chain) .sign(privateKey); } diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java index a7439ceacd048..591ca8c360f4d 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/TestUtils.java @@ -8,6 +8,7 @@ import java.util.List; import io.quarkus.oidc.runtime.OidcUtils; +import io.quarkus.oidc.runtime.TrustStoreUtils; import io.smallrye.jwt.build.Jwt; import io.smallrye.jwt.util.KeyUtils; import io.smallrye.jwt.util.ResourceUtils; @@ -36,6 +37,7 @@ public static String getAccessTokenWithCertChain(List chain, .groups("admin") .issuer("https://server.example.com") .audience("https://service.example.com") + .claim("root-certificate-thumbprint", TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1))) .jws().chain(chain) .sign(privateKey); } From fd34e4413a348826590de055c4792930e36fd292 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 15 Apr 2024 11:44:02 +0300 Subject: [PATCH 37/72] Introduce markers for static and runtime init recorder methods These are meant to replace the comments found in various recorder methods --- .../quarkus/runtime/annotations/RuntimeInit.java | 14 ++++++++++++++ .../io/quarkus/runtime/annotations/StaticInit.java | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 core/runtime/src/main/java/io/quarkus/runtime/annotations/RuntimeInit.java create mode 100644 core/runtime/src/main/java/io/quarkus/runtime/annotations/StaticInit.java diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/RuntimeInit.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/RuntimeInit.java new file mode 100644 index 0000000000000..9f57f399efdc6 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/RuntimeInit.java @@ -0,0 +1,14 @@ +package io.quarkus.runtime.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation used to indicate that a recorder method is called during the runtime init phase + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface RuntimeInit { +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/StaticInit.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/StaticInit.java new file mode 100644 index 0000000000000..90a3793ec2963 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/StaticInit.java @@ -0,0 +1,14 @@ +package io.quarkus.runtime.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation used to indicate that a recorder method is called during the static init phase + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface StaticInit { +} From 986ad73d2232b54bc9d597f277276c03961c1aa6 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 15 Apr 2024 10:45:36 +0200 Subject: [PATCH 38/72] Qute: fix NativeImageResourceBuildItem registration on Windows - previously, a NativeImageResourceBuildItem with a wrong path was produced for a template located in a nested directory --- .../qute/deployment/QuteProcessor.java | 103 ++++++++++++------ .../AdditionalTemplateRootTest.java | 8 +- 2 files changed, 78 insertions(+), 33 deletions(-) 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 917c311f6aee7..5a57f0d95f270 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 @@ -8,7 +8,6 @@ import static java.util.function.Predicate.not; import static java.util.stream.Collectors.toMap; -import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.StringReader; @@ -2149,15 +2148,17 @@ public boolean test(String path) { } for (Path resolvedPath : artifact.getResolvedPaths()) { if (Files.isDirectory(resolvedPath)) { - scanPath(resolvedPath, resolvedPath, config, templateRoots, watchedPaths, templatePaths, + scanRootPath(resolvedPath, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); } else { try (FileSystem artifactFs = ZipUtils.newFileSystem(resolvedPath)) { + // Iterate over template roots, such as "templates", and collect the included templates for (String templateRoot : templateRoots) { Path artifactBasePath = artifactFs.getPath(templateRoot); if (Files.exists(artifactBasePath)) { - LOGGER.debugf("Found extension templates in: %s", resolvedPath); - scan(artifactBasePath, artifactBasePath, templateRoot + "/", watchedPaths, templatePaths, + LOGGER.debugf("Found template root in extension artifact: %s", resolvedPath); + scanDirectory(artifactBasePath, artifactBasePath, templateRoot + "/", watchedPaths, + templatePaths, nativeImageResources, config); } @@ -2173,13 +2174,20 @@ public boolean test(String path) { for (Path root : tree.getRoots()) { // Note that we cannot use ApplicationArchive.getChildPath(String) here because we would not be able to detect // a wrong directory name on case-insensitive file systems - scanPath(root, root, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); + scanRootPath(root, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); } }); } } - private void scanPath(Path rootPath, Path path, QuteConfig config, TemplateRootsBuildItem templateRoots, + private void scanRootPath(Path rootPath, QuteConfig config, TemplateRootsBuildItem templateRoots, + BuildProducer watchedPaths, + BuildProducer templatePaths, + BuildProducer nativeImageResources) { + scanRootPath(rootPath, rootPath, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); + } + + private void scanRootPath(Path rootPath, Path path, QuteConfig config, TemplateRootsBuildItem templateRoots, BuildProducer watchedPaths, BuildProducer templatePaths, BuildProducer nativeImageResources) { @@ -2193,15 +2201,15 @@ private void scanPath(Path rootPath, Path path, QuteConfig config, TemplateRoots // "/io", "/META-INF", "/templates", "/web", etc. Path relativePath = rootPath.relativize(file); if (templateRoots.isRoot(relativePath)) { - LOGGER.debugf("Found templates dir: %s", file); - // The base path is an OS-specific path relative to the template root - String basePath = relativePath.toString() + File.separatorChar; - scan(file, file, basePath, watchedPaths, templatePaths, + LOGGER.debugf("Found templates root dir: %s", file); + // The base path is an OS-specific template root path relative to the scanned root path + String basePath = relativePath.toString() + relativePath.getFileSystem().getSeparator(); + scanDirectory(file, file, basePath, watchedPaths, templatePaths, nativeImageResources, config); } else if (templateRoots.maybeRoot(relativePath)) { // Scan the path recursively because the template root may be nested, for example "/web/public" - scanPath(rootPath, file, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); + scanRootPath(rootPath, file, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); } } } @@ -3384,33 +3392,54 @@ public static String getName(InjectionPointInfo injectionPoint) { throw new IllegalArgumentException(); } + /** + * + * @param templatePaths + * @param watchedPaths + * @param nativeImageResources + * @param osSpecificResourcePath The OS-specific resource path, i.e. templates\nested\foo.html + * @param templatePath The path relative to the template root; using the {@code /} path separator + * @param originalPath + * @param config + */ private static void produceTemplateBuildItems(BuildProducer templatePaths, BuildProducer watchedPaths, - BuildProducer nativeImageResources, String basePath, String filePath, + BuildProducer nativeImageResources, String osSpecificResourcePath, + String templatePath, Path originalPath, QuteConfig config) { - if (filePath.isEmpty()) { + if (templatePath.isEmpty()) { return; } - // OS-specific full path, i.e. templates\foo.html - String osSpecificPath = basePath + filePath; // OS-agnostic full path, i.e. templates/foo.html - String osAgnosticPath = osSpecificPath; - if (File.separatorChar != '/') { - osAgnosticPath = osAgnosticPath.replace(File.separatorChar, '/'); - } - LOGGER.debugf("Produce template build items [filePath: %s, fullPath: %s, originalPath: %s", filePath, osSpecificPath, + String osAgnosticResourcePath = toOsAgnosticPath(osSpecificResourcePath, originalPath.getFileSystem()); + LOGGER.debugf("Produce template build items [templatePath: %s, osSpecificResourcePath: %s, originalPath: %s", + templatePath, + osSpecificResourcePath, originalPath); boolean restartNeeded = true; if (config.devMode.noRestartTemplates.isPresent()) { - restartNeeded = !config.devMode.noRestartTemplates.get().matcher(osAgnosticPath).matches(); + restartNeeded = !config.devMode.noRestartTemplates.get().matcher(osAgnosticResourcePath).matches(); } - watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(osAgnosticPath, restartNeeded)); - nativeImageResources.produce(new NativeImageResourceBuildItem(osSpecificPath)); + watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(osAgnosticResourcePath, restartNeeded)); + nativeImageResources.produce(new NativeImageResourceBuildItem(osSpecificResourcePath)); templatePaths.produce( - new TemplatePathBuildItem(filePath, originalPath, readTemplateContent(originalPath, config.defaultCharset))); + new TemplatePathBuildItem(templatePath, originalPath, + readTemplateContent(originalPath, config.defaultCharset))); } - private void scan(Path root, Path directory, String basePath, BuildProducer watchedPaths, + /** + * + * @param root + * @param directory + * @param basePath OS-specific template root path relative to the scanned root path, e.g. {@code templates/} + * @param watchedPaths + * @param templatePaths + * @param nativeImageResources + * @param config + * @throws IOException + */ + private void scanDirectory(Path root, Path directory, String basePath, + BuildProducer watchedPaths, BuildProducer templatePaths, BuildProducer nativeImageResources, QuteConfig config) @@ -3431,24 +3460,36 @@ private void scan(Path root, Path directory, String basePath, BuildProducer> excludes) { for (Predicate exclude : excludes) { if (exclude.test(check)) { diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java index 7e26be68d7834..9095a01599387 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java @@ -28,6 +28,7 @@ public class AdditionalTemplateRootTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot(root -> root .addAsResource(new StringAsset("Hi {name}!"), "templates/hi.txt") + .addAsResource(new StringAsset("Hoho {name}!"), "templates/nested/hoho.txt") .addAsResource(new StringAsset("Hello {name}!"), "web/public/hello.txt")) .addBuildChainCustomizer(buildCustomizer()); @@ -52,11 +53,13 @@ public void execute(BuildContext context) { if (item.getResources().contains("web/public/hello.txt") || item.getResources().contains("web\\public\\hello.txt") || item.getResources().contains("templates/hi.txt") - || item.getResources().contains("templates\\hi.txt")) { + || item.getResources().contains("templates\\hi.txt") + || item.getResources().contains("templates/nested/hoho.txt") + || item.getResources().contains("templates\\nested\\hoho.txt")) { found++; } } - if (found != 2) { + if (found != 3) { throw new IllegalStateException(items.stream().flatMap(i -> i.getResources().stream()) .collect(Collectors.toList()).toString()); } @@ -79,6 +82,7 @@ public void execute(BuildContext context) { public void testTemplate() { assertEquals("Hi M!", engine.getTemplate("hi").data("name", "M").render()); assertEquals("Hello M!", hello.data("name", "M").render()); + assertEquals("Hoho M!", engine.getTemplate("nested/hoho").data("name", "M").render()); } } From 43e92f32c35c045eec94661d2fa923de1edb8926 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 15 Apr 2024 11:48:04 +0300 Subject: [PATCH 39/72] Replace comments about recorder method execution with markers --- .../opentelemetry/runtime/OpenTelemetryRecorder.java | 10 ++++++---- .../opentelemetry/runtime/tracing/TracerRecorder.java | 5 +++-- .../intrumentation/InstrumentationRecorder.java | 4 +++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java index 6598d2a15f0ac..0967428012cb2 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java @@ -20,6 +20,8 @@ import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.annotations.RuntimeInit; +import io.quarkus.runtime.annotations.StaticInit; import io.quarkus.runtime.configuration.DurationConverter; import io.smallrye.config.ConfigValue; import io.smallrye.config.SmallRyeConfig; @@ -30,23 +32,23 @@ public class OpenTelemetryRecorder { public static final String OPEN_TELEMETRY_DRIVER = "io.opentelemetry.instrumentation.jdbc.OpenTelemetryDriver"; - /* STATIC INIT */ + @StaticInit public void resetGlobalOpenTelemetryForDevMode() { GlobalOpenTelemetry.resetForTest(); GlobalEventEmitterProvider.resetForTest(); } - /* RUNTIME INIT */ + @RuntimeInit public void eagerlyCreateContextStorage() { ContextStorage.get(); } - /* RUNTIME INIT */ + @RuntimeInit public void storeVertxOnContextStorage(Supplier vertx) { QuarkusContextStorage.vertx = vertx.get(); } - /* RUNTIME INIT */ + @RuntimeInit public Function, OpenTelemetry> opentelemetryBean( OTelRuntimeConfig oTelRuntimeConfig) { return new Function<>() { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java index a0d5486ba0a3d..393aa22568fb1 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java @@ -9,6 +9,7 @@ import io.opentelemetry.semconv.ResourceAttributes; import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.annotations.StaticInit; @Recorder public class TracerRecorder { @@ -16,7 +17,7 @@ public class TracerRecorder { public static final Set dropNonApplicationUriTargets = new HashSet<>(); public static final Set dropStaticResourceTargets = new HashSet<>(); - /* STATIC INIT */ + @StaticInit public void setAttributes( BeanContainer beanContainer, String quarkusVersion, @@ -35,7 +36,7 @@ public void setAttributes( .getAttributes()); } - /* STATIC INIT */ + @StaticInit public void setupSampler( List dropNonApplicationUris, List dropStaticResources) { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java index debb921c7350b..11873ab929225 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/InstrumentationRecorder.java @@ -19,6 +19,7 @@ import io.quarkus.opentelemetry.runtime.tracing.intrumentation.vertx.SqlClientInstrumenterVertxTracer; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.annotations.RuntimeInit; import io.vertx.core.VertxOptions; import io.vertx.core.metrics.MetricsOptions; import io.vertx.core.tracing.TracingOptions; @@ -34,7 +35,7 @@ public InstrumentationRecorder(RuntimeValue config) { this.config = config; } - /* RUNTIME INIT */ + @RuntimeInit public Consumer getVertxTracingOptions() { TracingOptions tracingOptions = new TracingOptions() .setFactory(FACTORY); @@ -42,6 +43,7 @@ public Consumer getVertxTracingOptions() { } /* RUNTIME INIT */ + @RuntimeInit public void setupVertxTracer(BeanContainer beanContainer, boolean sqlClientAvailable, boolean redisClientAvailable, final String semconvStability) { OpenTelemetry openTelemetry = beanContainer.beanInstance(OpenTelemetry.class); From c80d8c52be4755e81e334e45b02ecfd08fec4b98 Mon Sep 17 00:00:00 2001 From: Andy Damevin Date: Mon, 15 Apr 2024 11:06:07 +0200 Subject: [PATCH 40/72] Make Qute doc more intuitive --- docs/src/main/asciidoc/qute.adoc | 42 ++++++++++++++++++++++++-------- docs/src/main/asciidoc/web.adoc | 2 +- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/docs/src/main/asciidoc/qute.adoc b/docs/src/main/asciidoc/qute.adoc index 8343262a59aae..d62aa854b9714 100644 --- a/docs/src/main/asciidoc/qute.adoc +++ b/docs/src/main/asciidoc/qute.adoc @@ -26,10 +26,14 @@ Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {q The solution is located in the `qute-quickstart` link:{quickstarts-tree-url}/qute-quickstart[directory]. -[[hello-qute-web]] -== Hello World with Qute Web +[[serving-templates]] +== Serving Qute templates via http + +If you want to serve your templates via http: + +1. The Qute Web extension allows you to directly serve via http templates located in `src/main/resource/templates/pub/`. In that case you don't need any Java code to "plug" the template, for example, the template `src/main/resource/templates/pub/foo.html` will be served from the paths `/foo` and `/foo.html` by default. +2. For finer control, you can combine it with Quarkus REST or Quarkus RESTEasy to control how your template will be served. All files located in the `src/main/resources/templates` directory and its subdirectories are registered as templates and can be injected in a REST resource. -If you want to use Qute in your Quarkus Web application, add the Qute Web extension first: [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- @@ -38,16 +42,19 @@ If you want to use Qute in your Quarkus Web application, add the Qute Web extens quarkus-qute-web ---- -+ + [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- implementation("io.quarkiverse.qute.web:quarkus-qute-web") ---- -All files located in the `src/main/resources/templates` directory and its subdirectories are registered as templates. Templates are validated during startup and watched for changes in the development mode. +NOTE: The Qute Web extension is still using the quarkiverse group-id, it is part of the Quarkus platform (so it's is included in the bom), we are working on moving it to Quarkus Core. + +[[hello-qute-web]] +=== Serving Hello World with Qute -Now, let's start with a Hello World html template: +Let's start with a Hello World template: .src/main/resources/templates/pub/hello.html [source] @@ -60,14 +67,29 @@ NOTE: Templates located in the `pub` directory are served via HTTP. Automaticall If your application is running, you can open your browser and hit: http://localhost:8080/hello?name=Martin -For more information about Qute Web, see the https://docs.quarkiverse.io/quarkus-qute-web/dev/index.html[Qute Web guide]. +For more information about Qute Web options, see the https://docs.quarkiverse.io/quarkus-qute-web/dev/index.html[Qute Web guide]. [[hello-qute-rest]] -== Hello World with Jakarta REST +=== Hello Qute and REST + +For finer control, you can combine Qute Web with Quarkus REST or Quarkus RESTEasy to control how your template will be served -If you want to use Qute in your Jakarta REST application, you still need to add the Qute Web extension first (see <>) and make sure you have the Quarkus REST (formerly RESTEasy Reactive) extension. +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-rest + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-rest") +---- -Here is a very simple text template: +A very simple text template: .hello.txt [source] diff --git a/docs/src/main/asciidoc/web.adoc b/docs/src/main/asciidoc/web.adoc index c3ecc9b68ed2a..1f58a62ef29fb 100644 --- a/docs/src/main/asciidoc/web.adoc +++ b/docs/src/main/asciidoc/web.adoc @@ -51,7 +51,7 @@ For templating and server-side rendering with Quarkus, there are different engin Qute is designed specifically to meet the Quarkus needs, and help you deal with templates, snippets, and partials and render the data from your storage. It is inspired by the most famous template engines, it is fast, type-safe, works in native, and has a lot of nice features. -To install Qute Web, follow xref:qute.adoc[the instructions]. +To install Qute Web, follow xref:qute.adoc#serving-templates[the instructions]. Here is a simple example of a Qute template: From 9f6626859f206ef95a6f55618b04cbe656cc1481 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Thu, 11 Apr 2024 10:01:47 -0300 Subject: [PATCH 41/72] Introduce TemplateInstance.setLocale Co-authored-by: Martin Kouba --- docs/src/main/asciidoc/qute-reference.adoc | 2 +- .../io/quarkus/qute/i18n/MessageBundles.java | 2 +- .../io/quarkus/qute/TemplateInstance.java | 33 ++++++++++++++++--- .../io/quarkus/qute/TemplateInstanceTest.java | 14 ++++++++ 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 942ecaef36afd..02831031a86df 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -2829,7 +2829,7 @@ public class MyBean { Template hello; String render() { - return hello.instance().setAttribute("locale", Locale.forLanguageTag("cs")).render(); <1> + return hello.instance().setLocale("cs").render(); <1> } } ---- diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java index b460ac5b144d8..6191405d631fc 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java @@ -28,7 +28,7 @@ public final class MessageBundles { - public static final String ATTRIBUTE_LOCALE = "locale"; + public static final String ATTRIBUTE_LOCALE = TemplateInstance.LOCALE; public static final String DEFAULT_LOCALE = "<>"; private static final Logger LOGGER = Logger.getLogger(MessageBundles.class); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java index 25d496ec4a206..d59acc169bfde 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java @@ -1,5 +1,6 @@ package io.quarkus.qute; +import java.util.Locale; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; import java.util.function.Function; @@ -31,6 +32,11 @@ public interface TemplateInstance { */ String SELECTED_VARIANT = "selectedVariant"; + /** + * Attribute key - locale. + */ + String LOCALE = "locale"; + /** * Set the the root data object. Invocation of this method removes any data set previously by * {@link #data(String, Object)} and {@link #computedData(String, Function)}. @@ -70,7 +76,6 @@ default TemplateInstance computedData(String key, Function funct } /** - * * @param key * @param value * @return self @@ -80,7 +85,6 @@ default TemplateInstance setAttribute(String key, Object value) { } /** - * * @param key * @return the attribute or null */ @@ -142,7 +146,6 @@ default CompletionStage consume(Consumer consumer) { } /** - * * @return the timeout * @see TemplateInstance#TIMEOUT */ @@ -151,7 +154,6 @@ default long getTimeout() { } /** - * * @return the original template */ default Template getTemplate() { @@ -159,7 +161,6 @@ default Template getTemplate() { } /** - * * @param id * @return the fragment or {@code null} * @see Template#getFragment(String) @@ -178,6 +179,28 @@ default TemplateInstance onRendered(Runnable action) { throw new UnsupportedOperationException(); } + /** + * Sets the {@code locale} attribute that can be used to localize parts of the template, i.e. to specify the locale for all + * message bundle expressions in the template. + * + * @param locale a language tag + * @return self + */ + default TemplateInstance setLocale(String locale) { + return setAttribute(LOCALE, Locale.forLanguageTag(locale)); + } + + /** + * Sets the {@code locale} attribute that can be used to localize parts of the template, i.e. to specify the locale for all + * message bundle expressions in the template. + * + * @param locale a {@link Locale} instance + * @return self + */ + default TemplateInstance setLocale(Locale locale) { + return setAttribute(LOCALE, locale); + } + /** * This component can be used to initialize a template instance, i.e. the data and attributes. * diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java index ba675fb3c6fe1..fceb3cc264836 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; +import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; @@ -64,4 +66,16 @@ public void testComputeData() { assertTrue(fooUsed.get()); assertFalse(barUsed.get()); } + + @Test + public void testLocale() throws Exception { + Engine engine = Engine.builder().addDefaults() + .addValueResolver(ValueResolver.builder() + .applyToName("locale") + .resolveSync(ctx -> ctx.getAttribute(TemplateInstance.LOCALE)) + .build()) + .build(); + Template hello = engine.parse("Hello {locale}!"); + assertEquals("Hello fr!", hello.instance().setLocale(Locale.FRENCH).render()); + } } From f0c5435f303451de8e49c3a9baee3c996b24c68a Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Fri, 12 Apr 2024 15:20:33 -0300 Subject: [PATCH 42/72] Introduce TemplateInstance.setVariant --- docs/src/main/asciidoc/qute-reference.adoc | 6 ++++-- .../java/io/quarkus/qute/TemplateInstance.java | 10 ++++++++++ .../io/quarkus/qute/TemplateInstanceTest.java | 18 +++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 02831031a86df..dea89afaf0357 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1659,7 +1659,7 @@ public class CustomLocator implements TemplateLocator { === Template Variants Sometimes it's useful to render a specific variant of the template based on the content negotiation. -This can be done by setting a special attribute via `TemplateInstance.setAttribute()`: +This can be done by setting a special attribute via `TemplateInstance.setVariant()`: [source,java] ---- @@ -1672,7 +1672,9 @@ class MyService { ItemManager manager; String renderItems() { - return items.data("items",manager.findItems()).setAttribute(TemplateInstance.SELECTED_VARIANT, new Variant(Locale.getDefault(),"text/html","UTF-8")).render(); + return items.data("items", manager.findItems()) + .setVariant(new Variant(Locale.getDefault(), "text/html", "UTF-8")) + .render(); } } ---- diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java index d59acc169bfde..5b5a2f1f9a42a 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java @@ -201,6 +201,16 @@ default TemplateInstance setLocale(Locale locale) { return setAttribute(LOCALE, locale); } + /** + * Sets the variant attribute that can be used to select a specific variant of the template. + * + * @param variant the variant + * @return self + */ + default TemplateInstance setVariant(Variant variant) { + return setAttribute(SELECTED_VARIANT, variant); + } + /** * This component can be used to initialize a template instance, i.e. the data and attributes. * diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java index fceb3cc264836..1ee22f343b255 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; @@ -78,4 +77,21 @@ public void testLocale() throws Exception { Template hello = engine.parse("Hello {locale}!"); assertEquals("Hello fr!", hello.instance().setLocale(Locale.FRENCH).render()); } + + @Test + public void testVariant() { + Engine engine = Engine.builder().addDefaults() + .addValueResolver(ValueResolver.builder() + .applyToName("variant") + .resolveSync(ctx -> ctx.getAttribute(TemplateInstance.SELECTED_VARIANT)) + .build()) + .addValueResolver(ValueResolver.builder() + .appliesTo(ctx -> ctx.getBase() instanceof Variant && ctx.getName().equals("contentType")) + .resolveSync(ctx -> ((Variant) ctx.getBase()).getContentType()) + .build()) + .build(); + Template hello = engine.parse("Hello {variant.contentType}!"); + String render = hello.instance().setVariant(Variant.forContentType(Variant.TEXT_HTML)).render(); + assertEquals("Hello text/html!", render); + } } From 6979a5a073de7607179156002c998aa829547dab Mon Sep 17 00:00:00 2001 From: Andy Damevin Date: Mon, 15 Apr 2024 14:36:29 +0200 Subject: [PATCH 43/72] Apply suggestions from code review Co-authored-by: Max Rydahl Andersen --- docs/src/main/asciidoc/qute.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/asciidoc/qute.adoc b/docs/src/main/asciidoc/qute.adoc index d62aa854b9714..1a20c1d74f0fa 100644 --- a/docs/src/main/asciidoc/qute.adoc +++ b/docs/src/main/asciidoc/qute.adoc @@ -32,7 +32,7 @@ The solution is located in the `qute-quickstart` link:{quickstarts-tree-url}/qut If you want to serve your templates via http: 1. The Qute Web extension allows you to directly serve via http templates located in `src/main/resource/templates/pub/`. In that case you don't need any Java code to "plug" the template, for example, the template `src/main/resource/templates/pub/foo.html` will be served from the paths `/foo` and `/foo.html` by default. -2. For finer control, you can combine it with Quarkus REST or Quarkus RESTEasy to control how your template will be served. All files located in the `src/main/resources/templates` directory and its subdirectories are registered as templates and can be injected in a REST resource. +2. For finer control, you can combine it with Quarkus REST to control how your template will be served. All files located in the `src/main/resources/templates` directory and its subdirectories are registered as templates and can be injected in a REST resource. [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml @@ -49,7 +49,7 @@ If you want to serve your templates via http: implementation("io.quarkiverse.qute.web:quarkus-qute-web") ---- -NOTE: The Qute Web extension is still using the quarkiverse group-id, it is part of the Quarkus platform (so it's is included in the bom), we are working on moving it to Quarkus Core. +NOTE: The Qute Web extension while using the quarkiverse group-id, it is still part of the Quarkus platform. [[hello-qute-web]] === Serving Hello World with Qute From 5d07fbd968a0254178b17f72f72e8a1421e1729e Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Thu, 11 Apr 2024 15:27:00 +0200 Subject: [PATCH 44/72] Fix various IT modules and add them to the CI matrices - Fix the gRPC Test Random Port IT (cannot use injection) - Fix the virtual thread webauthn IT (cannot use injection) - Fix the opentelemetry redis instrumentation IT (still unable to retrieve the exception) --- .github/native-tests.json | 10 ++-- .github/virtual-threads-tests.json | 6 +++ .../RandomPortSeparateServerPlainIT.java | 24 +++++++++ .../hello/RandomPortSeparateServerTlsIT.java | 9 ---- .../examples/hello/RandomPortTestBase.java | 2 + .../hello/RandomPortVertxServerPlainIT.java | 24 +++++++++ .../hello/RandomPortVertxServerTlsIT.java | 9 ---- .../reactive/mssql/DialectEndpoint.java | 9 ++-- .../HibernateReactiveMSSQLTestEndpoint.java | 7 +-- .../QuarkusOpenTelemetryRedisIT.java | 8 +-- .../QuarkusOpenTelemetryRedisTest.java | 9 +++- .../webauthn/RunOnVirtualThreadIT.java | 49 ++++++++++++++++++- .../webauthn/RunOnVirtualThreadTest.java | 2 +- 13 files changed, 132 insertions(+), 36 deletions(-) delete mode 100644 integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerTlsIT.java delete mode 100644 integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerTlsIT.java diff --git a/.github/native-tests.json b/.github/native-tests.json index 1b05bb4e97989..dbe32b8def327 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -93,7 +93,7 @@ { "category": "HTTP", "timeout": 110, - "test-modules": "elytron-resteasy, resteasy-jackson, elytron-resteasy-reactive, resteasy-mutiny, resteasy-reactive-kotlin/standard, vertx, vertx-http, vertx-web, vertx-web-jackson, vertx-graphql, virtual-http, rest-client, rest-client-reactive, rest-client-reactive-stork, rest-client-reactive-multipart, websockets, management-interface, management-interface-auth", + "test-modules": "elytron-resteasy, resteasy-jackson, elytron-resteasy-reactive, resteasy-mutiny, resteasy-reactive-kotlin/standard, vertx, vertx-http, vertx-web, vertx-web-jackson, vertx-graphql, virtual-http, rest-client, rest-client-reactive, rest-client-reactive-stork, rest-client-reactive-multipart, websockets, management-interface, management-interface-auth, mutiny-native-jctools", "os-name": "ubuntu-latest" }, { @@ -116,8 +116,8 @@ }, { "category": "Misc4", - "timeout": 125, - "test-modules": "picocli-native, gradle, micrometer-mp-metrics, micrometer-prometheus, logging-json, jaxp, jaxb, opentelemetry, opentelemetry-jdbc-instrumentation, webjars-locator", + "timeout": 130, + "test-modules": "picocli-native, gradle, micrometer-mp-metrics, micrometer-prometheus, logging-json, jaxp, jaxb, opentelemetry, opentelemetry-jdbc-instrumentation, opentelemetry-redis-instrumentation, webjars-locator", "os-name": "ubuntu-latest" }, { @@ -128,8 +128,8 @@ }, { "category": "gRPC", - "timeout": 75, - "test-modules": "grpc-health, grpc-interceptors, grpc-mutual-auth, grpc-plain-text-gzip, grpc-plain-text-mutiny, grpc-proto-v2, grpc-streaming, grpc-tls, grpc-tls-p12", + "timeout": 80, + "test-modules": "grpc-health, grpc-interceptors, grpc-mutual-auth, grpc-plain-text-gzip, grpc-plain-text-mutiny, grpc-proto-v2, grpc-streaming, grpc-tls, grpc-tls-p12, grpc-test-random-port", "os-name": "ubuntu-latest" }, { diff --git a/.github/virtual-threads-tests.json b/.github/virtual-threads-tests.json index 9a8a190876a79..a17c515aeeb5b 100644 --- a/.github/virtual-threads-tests.json +++ b/.github/virtual-threads-tests.json @@ -11,6 +11,12 @@ "timeout": 45, "test-modules": "amqp-virtual-threads, jms-virtual-threads, kafka-virtual-threads", "os-name": "ubuntu-latest" + }, + { + "category": "Security", + "timeout": 20, + "test-modules": "security-webauthn-virtual-threads", + "os-name": "ubuntu-latest" } ] } diff --git a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerPlainIT.java b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerPlainIT.java index 9e8da069b8d21..02b4a89c88323 100644 --- a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerPlainIT.java +++ b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerPlainIT.java @@ -1,9 +1,33 @@ package io.quarkus.grpc.examples.hello; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import com.google.common.net.HostAndPort; + +import examples.GreeterGrpc; +import examples.HelloRequest; +import io.grpc.netty.NettyChannelBuilder; import io.quarkus.test.junit.QuarkusIntegrationTest; import io.quarkus.test.junit.TestProfile; @QuarkusIntegrationTest @TestProfile(RandomPortSeparateServerPlainTestBase.Profile.class) class RandomPortSeparateServerPlainIT extends RandomPortSeparateServerPlainTestBase { + + @Test + void testWithNative() { + var channel = NettyChannelBuilder.forAddress("localhost", 9000).usePlaintext().build(); + var stub = GreeterGrpc.newBlockingStub(channel); + HelloRequest request = HelloRequest.newBuilder().setName("neo").build(); + var resp = stub.sayHello(request); + assertThat(resp.getMessage()).startsWith("Hello neo"); + + int clientPort = HostAndPort.fromString(channel.authority()).getPort(); + assertThat(clientPort).isNotEqualTo(0); + assertThat(clientPort).isEqualTo(9000); + + channel.shutdownNow(); + } } diff --git a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerTlsIT.java b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerTlsIT.java deleted file mode 100644 index 51853a2854b11..0000000000000 --- a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortSeparateServerTlsIT.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.quarkus.grpc.examples.hello; - -import io.quarkus.test.junit.QuarkusIntegrationTest; -import io.quarkus.test.junit.TestProfile; - -@QuarkusIntegrationTest -@TestProfile(RandomPortSeparateServerTlsTestBase.Profile.class) -class RandomPortSeparateServerTlsIT extends RandomPortSeparateServerTlsTestBase { -} diff --git a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortTestBase.java b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortTestBase.java index 5fe588155b622..5fe0786811fba 100644 --- a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortTestBase.java +++ b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortTestBase.java @@ -12,6 +12,7 @@ import examples.MutinyGreeterGrpc.MutinyGreeterStub; import io.grpc.Channel; import io.quarkus.grpc.GrpcClient; +import io.quarkus.test.junit.DisabledOnIntegrationTest; abstract class RandomPortTestBase { @GrpcClient("hello") @@ -21,6 +22,7 @@ abstract class RandomPortTestBase { Channel channel; @Test + @DisabledOnIntegrationTest void testRandomPort() { assertSoftly(softly -> { HelloRequest request = HelloRequest.newBuilder().setName("neo").build(); diff --git a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerPlainIT.java b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerPlainIT.java index bef37a3a9e053..701b6b0e85216 100644 --- a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerPlainIT.java +++ b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerPlainIT.java @@ -1,9 +1,33 @@ package io.quarkus.grpc.examples.hello; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import com.google.common.net.HostAndPort; + +import examples.GreeterGrpc; +import examples.HelloRequest; +import io.grpc.netty.NettyChannelBuilder; import io.quarkus.test.junit.QuarkusIntegrationTest; import io.quarkus.test.junit.TestProfile; @QuarkusIntegrationTest @TestProfile(RandomPortVertxServerPlainTestBase.Profile.class) class RandomPortVertxServerPlainIT extends RandomPortVertxServerPlainTestBase { + + @Test + void testWithNative() { + var channel = NettyChannelBuilder.forAddress("localhost", 8081).usePlaintext().build(); + var stub = GreeterGrpc.newBlockingStub(channel); + HelloRequest request = HelloRequest.newBuilder().setName("neo").build(); + var resp = stub.sayHello(request); + assertThat(resp.getMessage()).startsWith("Hello neo"); + + int clientPort = HostAndPort.fromString(channel.authority()).getPort(); + assertThat(clientPort).isNotEqualTo(0); + assertThat(clientPort).isEqualTo(8081); + + channel.shutdownNow(); + } } diff --git a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerTlsIT.java b/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerTlsIT.java deleted file mode 100644 index 632306895da84..0000000000000 --- a/integration-tests/grpc-test-random-port/src/test/java/io/quarkus/grpc/examples/hello/RandomPortVertxServerTlsIT.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.quarkus.grpc.examples.hello; - -import io.quarkus.test.junit.QuarkusIntegrationTest; -import io.quarkus.test.junit.TestProfile; - -@QuarkusIntegrationTest -@TestProfile(RandomPortVertxServerTlsTestBase.Profile.class) -class RandomPortVertxServerTlsIT extends RandomPortVertxServerTlsTestBase { -} diff --git a/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/DialectEndpoint.java b/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/DialectEndpoint.java index 85511daf4fef9..b3f51254a7fea 100644 --- a/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/DialectEndpoint.java +++ b/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/DialectEndpoint.java @@ -3,16 +3,17 @@ import java.io.IOException; import java.io.PrintWriter; -import org.hibernate.SessionFactory; -import org.hibernate.engine.spi.SessionFactoryImplementor; - -import io.quarkus.hibernate.orm.runtime.config.DialectVersions; import jakarta.inject.Inject; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.hibernate.SessionFactory; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +import io.quarkus.hibernate.orm.runtime.config.DialectVersions; + @WebServlet(name = "DialectEndpoint", urlPatterns = "/dialect/version") public class DialectEndpoint extends HttpServlet { @Inject diff --git a/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/HibernateReactiveMSSQLTestEndpoint.java b/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/HibernateReactiveMSSQLTestEndpoint.java index ef8aa9634fae0..1127443b989b9 100644 --- a/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/HibernateReactiveMSSQLTestEndpoint.java +++ b/integration-tests/hibernate-reactive-mssql/src/main/java/io/quarkus/it/hibernate/reactive/mssql/HibernateReactiveMSSQLTestEndpoint.java @@ -1,5 +1,9 @@ package io.quarkus.it.hibernate.reactive.mssql; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + import org.hibernate.reactive.mutiny.Mutiny; import io.smallrye.mutiny.Uni; @@ -7,9 +11,6 @@ import io.vertx.mutiny.sqlclient.Row; import io.vertx.mutiny.sqlclient.RowSet; import io.vertx.mutiny.sqlclient.Tuple; -import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; @Path("/tests") public class HibernateReactiveMSSQLTestEndpoint { diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisIT.java b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisIT.java index f67e195d26ce9..85463e6b768d0 100644 --- a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisIT.java +++ b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisIT.java @@ -1,12 +1,14 @@ package io.quarkus.it.opentelemetry; +import java.util.Map; + import io.quarkus.test.junit.QuarkusIntegrationTest; @QuarkusIntegrationTest class QuarkusOpenTelemetryRedisIT extends QuarkusOpenTelemetryRedisTest { - @Override - String getKey(String k) { - return "native-" + k; + void checkForException(Map exception) { + // Ignore it + // The exception is not passed in native mode. (need to be investigated) } } diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java index f10a0be951849..b0ed21891c967 100644 --- a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java +++ b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java @@ -117,6 +117,12 @@ public void syncInvalidOperation() { assertEquals("bazinga", span.get("name")); assertEquals("ERROR", status.get("statusCode")); assertEquals("exception", event.get("name")); + + checkForException(exception); + + } + + void checkForException(Map exception) { assertThat((String) exception.get("message"), containsString("ERR unknown command 'bazinga'")); } @@ -185,7 +191,8 @@ public void reactiveInvalidOperation() { assertEquals("bazinga", span.get("name")); assertEquals("ERROR", status.get("statusCode")); assertEquals("exception", event.get("name")); - assertThat((String) exception.get("message"), containsString("ERR unknown command 'bazinga'")); + + checkForException(exception); } private List> getSpans() { diff --git a/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadIT.java b/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadIT.java index c834a4ca97654..6ff6f8303ec7c 100644 --- a/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadIT.java +++ b/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadIT.java @@ -1,8 +1,55 @@ package io.quarkus.virtual.security.webauthn; +import static io.quarkus.virtual.security.webauthn.RunOnVirtualThreadTest.checkLoggedIn; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.quarkus.test.security.webauthn.WebAuthnEndpointHelper; +import io.quarkus.test.security.webauthn.WebAuthnHardware; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; +import io.vertx.core.json.JsonObject; @QuarkusIntegrationTest -class RunOnVirtualThreadIT extends RunOnVirtualThreadTest { +class RunOnVirtualThreadIT { + + @Test + public void test() { + + RestAssured.get("/open").then().statusCode(200).body(Matchers.is("Hello")); + RestAssured + .given().redirects().follow(false) + .get("/secure").then().statusCode(302); + RestAssured + .given().redirects().follow(false) + .get("/admin").then().statusCode(302); + RestAssured + .given().redirects().follow(false) + .get("/cheese").then().statusCode(302); + + CookieFilter cookieFilter = new CookieFilter(); + WebAuthnHardware hardwareKey = new WebAuthnHardware(); + String challenge = WebAuthnEndpointHelper.invokeRegistration("stef", cookieFilter); + JsonObject registration = hardwareKey.makeRegistrationJson(challenge); + + // now finalise + WebAuthnEndpointHelper.invokeCallback(registration, cookieFilter); + + // make sure our login cookie works + checkLoggedIn(cookieFilter); + + // reset cookies for the login phase + cookieFilter = new CookieFilter(); + // now try to log in + challenge = WebAuthnEndpointHelper.invokeLogin("stef", cookieFilter); + JsonObject login = hardwareKey.makeLoginJson(challenge); + + // now finalise + WebAuthnEndpointHelper.invokeCallback(login, cookieFilter); + // make sure our login cookie still works + checkLoggedIn(cookieFilter); + } } diff --git a/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadTest.java b/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadTest.java index 2cbd9f85afd5d..4d73fc4210d59 100644 --- a/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadTest.java +++ b/integration-tests/virtual-threads/security-webauthn-virtual-threads/src/test/java/io/quarkus/virtual/security/webauthn/RunOnVirtualThreadTest.java @@ -80,7 +80,7 @@ public void test() throws Exception { checkLoggedIn(cookieFilter); } - private void checkLoggedIn(CookieFilter cookieFilter) { + public static void checkLoggedIn(CookieFilter cookieFilter) { RestAssured .given() .filter(cookieFilter) From 2de7152c58609ccf01818adc16127605905e03aa Mon Sep 17 00:00:00 2001 From: Thomas Canava Date: Sat, 13 Apr 2024 00:27:26 +0200 Subject: [PATCH 45/72] Fix datasource devservices restarting quarkusio/quarkus#40015 --- .../deployment/devservices/DevServicesDatasourceProcessor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java index 559fcbe6fa3d0..a5ab0f15c4e3e 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java @@ -316,6 +316,8 @@ private RunningDevService startDevDb( e.getValue()); } } + setDataSourceProperties(propertiesMap, dbName, devServicesPrefix + "reuse", + String.valueOf(dataSourceBuildTimeConfig.devservices().reuse())); Map devDebProperties = new HashMap<>(); for (DevServicesDatasourceConfigurationHandlerBuildItem devDbConfigurationHandlerBuildItem : configHandlers) { From 7e229d433f5bb81a1ec1103678102a97dcebc562 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Wed, 10 Apr 2024 21:19:34 -0300 Subject: [PATCH 46/72] Unsign modified dependency JARs when filtering Add test for JarResultBuildStep#filterJarFile --- core/deployment/pom.xml | 24 +++++++ .../pkg/steps/JarResultBuildStep.java | 64 ++++++++++++------- .../pkg/steps/JarResultBuildStepTest.java | 34 ++++++++++ 3 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java diff --git a/core/deployment/pom.xml b/core/deployment/pom.xml index 0878028369f03..852e752a75c70 100644 --- a/core/deployment/pom.xml +++ b/core/deployment/pom.xml @@ -143,6 +143,30 @@ + + maven-dependency-plugin + + + download-signed-jar + generate-test-resources + + copy + + + + + org.eclipse.jgit + org.eclipse.jgit.ssh.apache + 6.9.0.202403050737-r + jar + signed.jar + + + ${project.build.testOutputDirectory} + + + + maven-surefire-plugin diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java index ade96118461b7..708dbb4e3c31e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java @@ -5,7 +5,6 @@ import java.io.BufferedInputStream; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectOutputStream; @@ -42,12 +41,12 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; import org.jboss.logging.Logger; @@ -92,14 +91,14 @@ /** * This build step builds both the thin jars and uber jars. - * + *

* The way this is built is a bit convoluted. In general, we only want a single one built, * as determined by the {@link PackageConfig} (unless the config explicitly asks for both of them) - * + *

* However, we still need an extension to be able to ask for a specific one of these despite the config, * e.g. if a serverless environment needs an uberjar to build its deployment package then we need * to be able to provide this. - * + *

* To enable this we have two build steps that strongly produce the respective artifact type build * items, but not a {@link ArtifactResultBuildItem}. We then * have another two build steps that only run if they are configured to consume these explicit @@ -929,7 +928,7 @@ private void copyDependency(Set parentFirstArtifacts, OutputTargetB } else { // we copy jars for which we remove entries to the same directory // which seems a bit odd to me - filterZipFile(resolvedDep, targetPath, removedFromThisArchive); + filterJarFile(resolvedDep, targetPath, removedFromThisArchive); } } } @@ -1123,7 +1122,7 @@ private void copyLibraryJars(FileSystem runnerZipFs, OutputTargetBuildItem outpu + resolvedDep.getFileName(); final Path targetPath = libDir.resolve(fileName); classPath.append(" lib/").append(fileName); - filterZipFile(resolvedDep, targetPath, transformedFromThisArchive); + filterJarFile(resolvedDep, targetPath, transformedFromThisArchive); } } else { // This case can happen when we are building a jar from inside the Quarkus repository @@ -1237,16 +1236,26 @@ private void handleParent(FileSystem runnerZipFs, String fileName, Map transformedFromThisArchive) { - + static void filterJarFile(Path resolvedDep, Path targetPath, Set transformedFromThisArchive) { try { byte[] buffer = new byte[10000]; - try (ZipFile in = new ZipFile(resolvedDep.toFile())) { - try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(targetPath.toFile()))) { - Enumeration entries = in.entries(); + try (JarFile in = new JarFile(resolvedDep.toFile(), false)) { + Manifest manifest = in.getManifest(); + if (manifest != null) { + // Remove signature entries + manifest.getEntries().clear(); + } else { + manifest = new Manifest(); + } + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(targetPath), manifest)) { + Enumeration entries = in.entries(); while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (!transformedFromThisArchive.contains(entry.getName())) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + if (!transformedFromThisArchive.contains(entryName) + && !entryName.equals(JarFile.MANIFEST_NAME) + && !entryName.equals("META-INF/INDEX.LIST") + && !isSignatureFile(entryName)) { entry.setCompressedSize(-1); out.putNextEntry(entry); try (InputStream inStream = in.getInputStream(entry)) { @@ -1255,6 +1264,8 @@ private void filterZipFile(Path resolvedDep, Path targetPath, Set transf out.write(buffer, 0, r); } } + } else { + log.debugf("Removed %s from %s", entryName, resolvedDep); } } } @@ -1262,10 +1273,21 @@ private void filterZipFile(Path resolvedDep, Path targetPath, Set transf Files.setLastModifiedTime(targetPath, Files.getLastModifiedTime(resolvedDep)); } } catch (IOException e) { - throw new RuntimeException(e); + throw new UncheckedIOException(e); } } + private static boolean isSignatureFile(String entry) { + entry = entry.toUpperCase(); + if (entry.startsWith("META-INF/") && entry.indexOf('/', "META-INF/".length()) == -1) { + return entry.endsWith(".SF") + || entry.endsWith(".DSA") + || entry.endsWith(".RSA") + || entry.endsWith(".EC"); + } + return false; + } + /** * Manifest generation is quite simple : we just have to push some attributes in manifest. * However, it gets a little more complex if the manifest preexists. @@ -1591,12 +1613,8 @@ public boolean downloadIfNecessary() { "https://repo.maven.apache.org/maven2/org/vineflower/vineflower/%s/vineflower-%s.jar", context.versionStr, context.versionStr); try (BufferedInputStream in = new BufferedInputStream(new URL(downloadURL).openStream()); - FileOutputStream fileOutputStream = new FileOutputStream(decompilerJar.toFile())) { - byte[] dataBuffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { - fileOutputStream.write(dataBuffer, 0, bytesRead); - } + OutputStream fileOutputStream = Files.newOutputStream(decompilerJar)) { + in.transferTo(fileOutputStream); return true; } catch (IOException e) { log.error("Unable to download Vineflower from " + downloadURL, e); diff --git a/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java new file mode 100644 index 0000000000000..7cfb2c4ece496 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/pkg/steps/JarResultBuildStepTest.java @@ -0,0 +1,34 @@ +package io.quarkus.deployment.pkg.steps; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Test for {@link JarResultBuildStep} + */ +class JarResultBuildStepTest { + + @Test + void should_unsign_jar_when_filtered(@TempDir Path tempDir) throws Exception { + Path signedJarFilePath = Path.of(getClass().getClassLoader().getResource("signed.jar").toURI()); + Path jarFilePath = tempDir.resolve("unsigned.jar"); + JarResultBuildStep.filterJarFile(signedJarFilePath, jarFilePath, + Set.of("org/eclipse/jgit/transport/sshd/SshdSessionFactory.class")); + try (JarFile jarFile = new JarFile(jarFilePath.toFile())) { + assertThat(jarFile.stream().map(JarEntry::getName)).doesNotContain("META-INF/ECLIPSE_.RSA", "META-INF/ECLIPSE_.SF"); + // Check that the manifest is still present + Manifest manifest = jarFile.getManifest(); + assertThat(manifest.getMainAttributes()).isNotEmpty(); + assertThat(manifest.getEntries()).isEmpty(); + } + } + +} From 82f7b20709255ea7e08aa8237eee0aba41903a91 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 15 Apr 2024 18:11:25 +0200 Subject: [PATCH 47/72] Rewrite footnotes for downstream documentation The modern version of footnotes is not supported in the downstream tooling and we can't use the older version in our doc as it triggers warnings. Thus we rewrite the new format to the old format specifically for downstream doc. --- .../AssembleDownstreamDocumentation.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java b/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java index f2438f7f08a56..6f23fd809cd4c 100755 --- a/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java +++ b/docs/src/main/java/io/quarkus/docs/generation/AssembleDownstreamDocumentation.java @@ -61,6 +61,7 @@ public class AssembleDownstreamDocumentation { Pattern.CASE_INSENSITIVE + Pattern.MULTILINE); private static final String SOURCE_BLOCK_PREFIX = "[source"; private static final String SOURCE_BLOCK_DELIMITER = "--"; + private static final Pattern FOOTNOTE_PATTERN = Pattern.compile("footnote:([a-z0-9_-]+)\\[(\\])?"); private static final String PROJECT_NAME_ATTRIBUTE = "{project-name}"; private static final String RED_HAT_BUILD_OF_QUARKUS = "Red Hat build of Quarkus"; @@ -386,7 +387,7 @@ private static void copyAsciidoc(Path sourceFile, Path targetFile, Set d if (currentBuffer.length() > 0) { rewrittenGuide.append( - rewriteLinks(sourceFile.getFileName().toString(), currentBuffer.toString(), downstreamGuides, + rewriteContent(sourceFile.getFileName().toString(), currentBuffer.toString(), downstreamGuides, titlesByReference, linkRewritingErrors)); currentBuffer.setLength(0); } @@ -399,7 +400,7 @@ private static void copyAsciidoc(Path sourceFile, Path targetFile, Set d if (currentBuffer.length() > 0) { rewrittenGuide.append( - rewriteLinks(sourceFile.getFileName().toString(), currentBuffer.toString(), downstreamGuides, + rewriteContent(sourceFile.getFileName().toString(), currentBuffer.toString(), downstreamGuides, titlesByReference, linkRewritingErrors)); } @@ -413,7 +414,7 @@ private static void copyAsciidoc(Path sourceFile, Path targetFile, Set d Files.writeString(targetFile, rewrittenGuideWithoutTabs.trim()); } - private static String rewriteLinks(String fileName, + private static String rewriteContent(String fileName, String content, Set downstreamGuides, Map titlesByReference, @@ -454,6 +455,14 @@ private static String rewriteLinks(String fileName, return "[[" + mr.group(1) + "]]"; }); + content = FOOTNOTE_PATTERN.matcher(content).replaceAll(mr -> { + if (mr.group(2) != null) { + return "footnoteref:[" + mr.group(1) + "]"; + } + + return "footnoteref:[" + mr.group(1) + ", "; + }); + return content; } From 37402641c19bdeaffe67a7a84164617e02c87a41 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 16 Apr 2024 08:34:50 +0300 Subject: [PATCH 48/72] Remove useless section in REST Client doc --- docs/src/main/asciidoc/rest-client.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index 4df2562e291fe..7e587807e0f9e 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -1386,8 +1386,6 @@ public interface EchoClient { [[multipart]] == Multipart Form support -REST Client support multipart messages. - === Sending Multipart messages REST Client allows sending data as multipart forms. This way you can for example From 3adb83840a443b01867ec87caeca5ed9967ba7e8 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 16 Apr 2024 09:31:28 +0300 Subject: [PATCH 49/72] Support FileUpload as multipart type in REST Client Closes: #40052 --- .../JaxrsClientReactiveProcessor.java | 14 +++++ .../multipart/MultipartDetectionTest.java | 37 +++++++++++++ .../multipart/MultipartFilenameTest.java | 52 +++++++++++++++++++ .../client/api/ClientMultipartForm.java | 6 +++ .../reactive/multipart/FileUpload.java | 2 +- 5 files changed, 110 insertions(+), 1 deletion(-) diff --git a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 3816b652ac553..9529ffff88426 100644 --- a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -121,6 +121,7 @@ import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; import org.jboss.resteasy.reactive.common.processor.scanning.ResourceScanningResult; import org.jboss.resteasy.reactive.multipart.FileDownload; +import org.jboss.resteasy.reactive.multipart.FileUpload; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; @@ -190,6 +191,7 @@ public class JaxrsClientReactiveProcessor { private static final String PATH_SIGNATURE = "L" + java.nio.file.Path.class.getName().replace('.', '/') + ";"; private static final String BUFFER_SIGNATURE = "L" + Buffer.class.getName().replace('.', '/') + ";"; private static final String BYTE_ARRAY_SIGNATURE = "[B"; + private static final String FILE_UPLOAD_SIGNATURE = "L" + FileUpload.class.getName().replace('.', '/') + ";"; private static final Logger log = Logger.getLogger(JaxrsClientReactiveProcessor.class); @@ -1176,6 +1178,7 @@ private boolean isMultipartRequiringType(String signature, String partType) { || signature.equals(BUFFER_SIGNATURE) || signature.equals(BYTE_ARRAY_SIGNATURE) || signature.equals(MULTI_BYTE_SIGNATURE) + || signature.equals(FILE_UPLOAD_SIGNATURE) || partType != null); } @@ -1793,6 +1796,8 @@ private void handleMultipartField(String formParamName, String partType, String } else if (type.equals(Path.class.getName())) { // and so is path addFile(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue); + } else if (type.equals(FileUpload.class.getName())) { + addFileUpload(fieldValue, multipartForm, methodCreator); } else if (type.equals(InputStream.class.getName())) { // and so is path addInputStream(ifValueNotNull, multipartForm, formParamName, partType, partFilename, fieldValue, type); @@ -1888,6 +1893,15 @@ private void addFile(BytecodeCreator methodCreator, AssignableResultHandle multi } } + private void addFileUpload(ResultHandle fieldValue, AssignableResultHandle multipartForm, + BytecodeCreator methodCreator) { + // MultipartForm#fileUpload(FileUpload fileUpload); + methodCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(ClientMultipartForm.class, "fileUpload", + ClientMultipartForm.class, FileUpload.class), + multipartForm, fieldValue); + } + private ResultHandle primitiveToString(BytecodeCreator methodCreator, ResultHandle fieldValue, FieldInfo field) { PrimitiveType primitiveType = field.type().asPrimitiveType(); switch (primitiveType.primitive()) { diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java index ab0a0675b8de3..b3fd11d635c78 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartDetectionTest.java @@ -81,6 +81,39 @@ void shouldCallImplicitEndpoints() throws IOException { .isEqualTo(file.getName() + " file Hello"); assertThat(client.postMultipartEntityImplicit(file.getName(), person)) .isEqualTo(file.getName() + " Stef:Epardaud"); + + assertThat(client.postMultipartImplicitFileUpload("Foo", new FileUpload() { + @Override + public String name() { + return "file"; + } + + @Override + public java.nio.file.Path filePath() { + return file.toPath(); + } + + @Override + public String fileName() { + return file.getName(); + } + + @Override + public long size() { + return -1; + } + + @Override + public String contentType() { + return "application/octet-stream"; + } + + @Override + public String charSet() { + return ""; + } + })) + .isEqualTo("Foo " + file.getName() + " Hello"); } @Path("form") @@ -142,6 +175,10 @@ String postMultipartEntityImplicit(@RestForm String name, @Consumes(MediaType.MULTIPART_FORM_DATA) String postMultipartExplicit(@RestForm String name, @RestForm File file); + @Path("multipart") + @POST + String postMultipartImplicitFileUpload(@RestForm String name, @RestForm FileUpload file); + @Path("urlencoded") @POST String postUrlencodedImplicit(@RestForm String name); diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java index 2a70e77f31ff7..69bec01287d09 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultipartFilenameTest.java @@ -60,6 +60,49 @@ void shouldPassOriginalFileName() throws IOException { assertThat(client.postMultipart(form)).isEqualTo(file.getName()); } + @Test + void shouldWorkWithFileUpload() throws IOException { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + File file = File.createTempFile("MultipartTest", ".txt"); + file.deleteOnExit(); + + ClientFormUsingFileUpload form = new ClientFormUsingFileUpload(); + form.file = new FileUpload() { + + @Override + public String name() { + return "myFile"; + } + + @Override + public java.nio.file.Path filePath() { + return file.toPath(); + } + + @Override + public String fileName() { + return file.getName(); + } + + @Override + public long size() { + return 0; + } + + @Override + public String contentType() { + return "application/octet-stream"; + } + + @Override + public String charSet() { + return ""; + } + }; + assertThat(client.postMultipartFileUpload(form)).isEqualTo(file.getName()); + } + @Test void shouldUseFileNameFromAnnotation() throws IOException { Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); @@ -244,6 +287,10 @@ public interface Client { @Consumes(MediaType.MULTIPART_FORM_DATA) String postMultipart(@MultipartForm ClientForm clientForm); + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + String postMultipartFileUpload(ClientFormUsingFileUpload clientForm); + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) String postMultipartWithPartFilename(@MultipartForm ClientFormUsingFile clientForm); @@ -324,6 +371,11 @@ public static class ClientForm { public File file; } + public static class ClientFormUsingFileUpload { + @RestForm + public FileUpload file; + } + public static class ClientFormUsingFile { @FormParam("myFile") @PartType(APPLICATION_OCTET_STREAM) diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java index 7b7bc74d9c220..b2fcbb5852922 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/ClientMultipartForm.java @@ -7,6 +7,7 @@ import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartForm; import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartFormDataPart; +import org.jboss.resteasy.reactive.multipart.FileUpload; import io.smallrye.mutiny.Multi; import io.vertx.core.buffer.Buffer; @@ -86,4 +87,9 @@ public ClientMultipartForm multiAsTextFileUpload(String name, String filename, M return this; } + public ClientMultipartForm fileUpload(FileUpload fileUpload) { + binaryFileUpload(fileUpload.name(), fileUpload.fileName(), fileUpload.filePath().toString(), fileUpload.contentType()); + return this; + } + } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java index 57a2bc9996ffa..b844cf4eab250 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/multipart/FileUpload.java @@ -5,7 +5,7 @@ /** * Represent a file that has been uploaded. *

- * WARNING: This type is currently only supported on the server + * This type is usually used on server, but it is also supported in the REST Client. */ public interface FileUpload extends FilePart { From b1cc21be8e22a1ec9d3fc33da3ea98ce10db4aed Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 16 Apr 2024 10:11:21 +0300 Subject: [PATCH 50/72] Update multipart support documentation --- docs/src/main/asciidoc/rest-client.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index 7e587807e0f9e..3f06f8291c7e0 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -1400,7 +1400,7 @@ To send data as a multipart form, you can just use the regular `@RestForm` (or ` String sendMultipart(@RestForm File file, @RestForm String otherField); ---- -Parameters specified as `File`, `Path`, `byte[]` or `Buffer` are sent as files and default to the +Parameters specified as `File`, `Path`, `byte[]`, `Buffer` or `FileUpload` are sent as files and default to the `application/octet-stream` MIME type. Other `@RestForm` parameter types default to the `text/plain` MIME type. You can override these defaults with the `@PartType` annotation. @@ -1421,7 +1421,7 @@ Naturally, you can also group these parameters into a containing class: String sendMultipart(Parameters parameters); ---- -Any `@RestForm` parameter of the type `File`, `Path`, `byte[]` or `Buffer`, as well as any +Any `@RestForm` parameter of the type `File`, `Path`, `byte[]`, `Buffer` or `FileUpload`, as well as any annotated with `@PartType` automatically imply a `@Consumes(MediaType.MULTIPART_FORM_DATA)` on the method if there is no `@Consumes` present. @@ -1533,7 +1533,7 @@ public ClientMultipartForm buildClientMultipartForm(Request request) { // <1> ClientMultipartForm multiPartForm = ClientMultipartForm.create(); multiPartForm.attribute("jsonPayload", request.getJsonPayload(), "jsonPayload"); // <2> request.getFiles().forEach(fu -> { - multiPartForm.binaryFileUpload("file", fu.name(), fu.filePath().toString(), fu.contentType()); // <3> + multiPartForm.fileUpload(fu); // <3> }); return multiPartForm; } @@ -1541,7 +1541,7 @@ public ClientMultipartForm buildClientMultipartForm(Request request) { // <1> <1> `Request` representing the request the server parts accepts <2> A `jsonPayload` attribute is added directly to `ClientMultipartForm` -<3> A `binaryFileUpload` is created from the request's `FileUpload` (which is a Quarkus REST (Server) type used to represent a binary file upload) +<3> A `fileUpload` is created from the request's `FileUpload` [NOTE] ==== From 561212eb4375769e316a10c9d8d4cd03e6fd6d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Mal=C3=A9=C5=99?= Date: Mon, 15 Apr 2024 14:30:11 +0200 Subject: [PATCH 51/72] Applying the QE feedback to the Logging guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michal Maléř --- docs/src/main/asciidoc/logging.adoc | 31 +++++++++++-------- .../json/runtime/AdditionalFieldConfig.java | 2 +- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/src/main/asciidoc/logging.adoc b/docs/src/main/asciidoc/logging.adoc index 0f6ab858131f0..84bef6c0cc3d1 100644 --- a/docs/src/main/asciidoc/logging.adoc +++ b/docs/src/main/asciidoc/logging.adoc @@ -74,6 +74,7 @@ The same flow can be applied with any of the <>. - -Configure the runtime logging in the `application.properties` file. +JBoss Logging, integrated into Quarkus, offers a unified configuration for all <> through a single configuration file that sets up all available extensions. +To adjust runtime logging, modify the `application.properties` file. .An example of how you can set the default log level to `INFO` logging and include Hibernate `DEBUG` logs: [source, properties] @@ -347,9 +349,9 @@ The logging format string supports the following symbols: |%t|Thread name|Render the thread name. |%t{id}|Thread ID|Render the thread ID. |%z{}|Time zone|Set the time zone of the output to ``. -|%X{}|Mapped Diagnostic Context Value|Renders the value from Mapped Diagnostic Context -|%X|Mapped Diagnostic Context Values|Renders all the values from Mapped Diagnostic Context in format {property.key=property.value} -|%x|Nested Diagnostics context values|Renders all the values from Nested Diagnostics Context in format {value1.value2} +|%X{}|Mapped Diagnostic Context Value|Renders the value from Mapped Diagnostic Context. +|%X|Mapped Diagnostic Context Values|Renders all the values from Mapped Diagnostic Context in format `{property.key=property.value}`. +|%x|Nested Diagnostics context values|Renders all the values from Nested Diagnostics Context in format `{value1.value2}`. |=== @@ -364,8 +366,8 @@ Changing the console log format is useful, for example, when the console output The `quarkus-logging-json` extension may be employed to add support for the JSON logging format and its related configuration. -Add this extension to your build file as the following snippet illustrates: - +. Add this extension to your build file as the following snippet illustrates: ++ [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- @@ -374,20 +376,21 @@ Add this extension to your build file as the following snippet illustrates: quarkus-logging-json ---- - ++ [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- implementation("io.quarkus:quarkus-logging-json") ---- - ++ By default, the presence of this extension replaces the output format configuration from the console configuration, and the format string and the color settings (if any) are ignored. The other console configuration items, including those controlling asynchronous logging and the log level, will continue to be applied. - ++ For some, it will make sense to use humanly readable (unstructured) logging in dev mode and JSON logging (structured) in production mode. This can be achieved using different profiles, as shown in the following configuration. - -.Disable JSON logging in application.properties for dev and test mode ++ +. Disable JSON logging in application.properties for dev and test mode: ++ [source, properties] ---- %dev.quarkus.log.console.json=false @@ -514,6 +517,8 @@ To register a logging filter: .An example of writing a filter: [source,java] ---- +package com.example; + import io.quarkus.logging.LoggingFilter; import java.util.logging.Filter; import java.util.logging.LogRecord; diff --git a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/AdditionalFieldConfig.java b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/AdditionalFieldConfig.java index ec95d7fb5a59c..dba33ea8e0d72 100644 --- a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/AdditionalFieldConfig.java +++ b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/AdditionalFieldConfig.java @@ -16,7 +16,7 @@ public class AdditionalFieldConfig { /** * Additional field type specification. - * Supported types: string, int, long + * Supported types: {@code string}, {@code int}, and {@code long}. * String is the default if not specified. */ @ConfigItem(defaultValue = "string") From 2c0d16b2b2c7e6a865a5f626256cf24079abf169 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 16 Apr 2024 09:49:06 +0200 Subject: [PATCH 52/72] ArC: skip warning about invalid startup for producer methods - resolves #40083 --- .../java/io/quarkus/arc/deployment/StartupBuildSteps.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java index 477eee7c6d506..53923c2136643 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java @@ -149,8 +149,12 @@ void registerStartupObservers(ObserverRegistrationPhaseBuildItem observerRegistr && !annotationStore.hasAnnotation(method, DotNames.PRODUCES)) { startupMethods.add(method); } else { - LOG.warnf("Ignored an invalid @Startup method declared on %s: %s", method.declaringClass().name(), - method); + if (!annotationStore.hasAnnotation(method, DotNames.PRODUCES)) { + // Producer methods annotated with @Startup are valid and processed above + LOG.warnf("Ignored an invalid @Startup method declared on %s: %s", + method.declaringClass().name(), + method); + } } } } From ff346be19ee01bdeeb2468030d91568ef1a82ee9 Mon Sep 17 00:00:00 2001 From: Yoshikazu Nojima Date: Tue, 16 Apr 2024 10:55:48 +0900 Subject: [PATCH 53/72] Correct asciidoc syntax error --- .../main/asciidoc/security-oidc-code-flow-authentication.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc index 6eb16fe378aef..034cf992124ee 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -689,7 +689,7 @@ You can disable token encryption in the session cookie by setting `quarkus.oidc. [[custom-token-state-manager]] ==== Session cookie and custom TokenStateManager -If you want to customize the way the tokens are associated with the session cookie, register a custom `io.quarkus.oidc.TokenStateManager' implementation as an `@ApplicationScoped` CDI bean. +If you want to customize the way the tokens are associated with the session cookie, register a custom `io.quarkus.oidc.TokenStateManager` implementation as an `@ApplicationScoped` CDI bean. For example, you might want to keep the tokens in a cache cluster and have only a key stored in a session cookie. Note that this approach might introduce some challenges if you need to make the tokens available across multiple microservices nodes. @@ -763,7 +763,7 @@ To use this feature, add the following extension to your project: :add-extension-extensions: oidc-db-token-state-manager include::{includes}/devtools/extension-add.adoc[] -This extension will replace the default `io.quarkus.oidc.TokenStateManager' with a database-based one. +This extension will replace the default `io.quarkus.oidc.TokenStateManager` with a database-based one. OIDC Database Token State Manager uses a Reactive SQL client under the hood to avoid blocking because the authentication is likely to happen on an IO thread. From 8b1094ec0a2fc850366e83e0f5c5fd5e65af1ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bresson?= Date: Mon, 15 Apr 2024 14:50:27 +0200 Subject: [PATCH 54/72] docs: mention logging in smallrye-graphql-client --- docs/src/main/asciidoc/smallrye-graphql-client.adoc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/src/main/asciidoc/smallrye-graphql-client.adoc b/docs/src/main/asciidoc/smallrye-graphql-client.adoc index de25e13e8ff09..4aa0a118e0d6b 100644 --- a/docs/src/main/asciidoc/smallrye-graphql-client.adoc +++ b/docs/src/main/asciidoc/smallrye-graphql-client.adoc @@ -200,6 +200,7 @@ public class Planet { Now that we have the model classes, we can create the interface that represents the actual set of operations we want to call on the remote GraphQL service. +[source,java] ---- @GraphQLClientApi(configKey = "star-wars-typesafe") public interface StarWarsClientApi { @@ -256,6 +257,18 @@ With this REST endpoint included in your application, you can simply send a GET and the application will use an injected typesafe client instance to call the remote service, obtain the films and planets, and return the JSON representation of the resulting list. +=== Logging + +For debugging purpose, it is possible to log the request generated by the typesafe client and the response sent back by the server by changing the log level of the `io.smallrye.graphql.client` category to `TRACE` (see the xref:logging.adoc#configure-the-log-level-category-and-format[Logging guide] for more details about how to configure logging). + +This can be achieved by adding the following lines to the `application.properties`: + +[source,properties] +---- +quarkus.log.category."io.smallrye.graphql.client".level=TRACE +quarkus.log.category."io.smallrye.graphql.client".min-level=TRACE +---- + == Using the Dynamic client For the dynamic client, the model classes are optional, because we can work with abstract From d56a6254661b8f568945d8f28a567ef01204be3c Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 16 Apr 2024 11:30:54 +0200 Subject: [PATCH 55/72] Qute: type-safe messages - add test for localized enum - related to #40089 - also use the convenient TemplateInstance#setLocale() method --- .../deployment/MessageBundleProcessor.java | 9 +++++-- .../i18n/MessageBundleDefaultedNameTest.java | 6 +---- .../i18n/MessageBundleLocaleTest.java | 6 +---- .../i18n/MessageBundleLogicalLineTest.java | 27 ++++++++++++++----- .../deployment/i18n/MessageBundleTest.java | 2 +- .../test/resources/messages/msg_cs.properties | 11 ++++++-- 6 files changed, 39 insertions(+), 22 deletions(-) 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 78be8027ee3d0..56e4529393b9b 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 @@ -789,8 +789,13 @@ private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundle Map keyToTemplate = new HashMap<>(); for (ListIterator it = Files.readAllLines(localizedFile).listIterator(); it.hasNext();) { String line = it.next(); - if (line.startsWith("#") || line.isBlank()) { - // Comments and blank lines are skipped + if (line.isBlank()) { + // Blank lines are skipped + continue; + } + line = line.strip(); + if (line.startsWith("#")) { + // Comments are skipped continue; } int eqIdx = line.indexOf('='); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleDefaultedNameTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleDefaultedNameTest.java index c171a5f3e1a22..1db4062826838 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleDefaultedNameTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleDefaultedNameTest.java @@ -2,8 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Locale; - import jakarta.inject.Inject; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -13,7 +11,6 @@ import io.quarkus.qute.Engine; import io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; -import io.quarkus.qute.i18n.MessageBundles; import io.quarkus.test.QuarkusUnitTest; public class MessageBundleDefaultedNameTest { @@ -43,8 +40,7 @@ public class MessageBundleDefaultedNameTest { public void testBundles() { assertEquals("Hello world!", Controller.Templates.index("world").render()); - assertEquals("Ahoj svete!", Controller.Templates.index("svete") - .setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render()); + assertEquals("Ahoj svete!", Controller.Templates.index("svete").setLocale("cs").render()); assertEquals("Hello world!", engine.getTemplate("app").render()); assertEquals("Hello alpha!", engine.getTemplate("alpha").render()); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLocaleTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLocaleTest.java index 2131cc87cac11..31d2bdcf22a25 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLocaleTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLocaleTest.java @@ -2,8 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Locale; - import jakarta.inject.Inject; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -13,7 +11,6 @@ import io.quarkus.qute.Template; import io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; -import io.quarkus.qute.i18n.MessageBundles; import io.quarkus.test.QuarkusUnitTest; public class MessageBundleLocaleTest { @@ -31,8 +28,7 @@ public class MessageBundleLocaleTest { @Test public void testResolvers() { - assertEquals("Ahoj svete!", - foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render()); + assertEquals("Ahoj svete!", foo.instance().setLocale("cs").render()); } @MessageBundle(locale = "cs") diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java index cd6d3f735c280..89c944458e999 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java @@ -3,8 +3,6 @@ import static io.quarkus.qute.i18n.MessageBundle.DEFAULT_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Locale; - import jakarta.inject.Inject; import org.jboss.shrinkwrap.api.asset.StringAsset; @@ -12,9 +10,9 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateEnum; import io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; -import io.quarkus.qute.i18n.MessageBundles; import io.quarkus.test.QuarkusUnitTest; public class MessageBundleLogicalLineTest { @@ -25,7 +23,7 @@ public class MessageBundleLogicalLineTest { .addClasses(Messages.class) .addAsResource("messages/msg_cs.properties") .addAsResource(new StringAsset( - "{msg:hello('Edgar')} {msg:helloNextLine('Edgar')} ::{msg:fruits}"), + "{msg:hello('Edgar')}::{msg:helloNextLine('Edgar')}::{msg:fruits}::{msg:myEnum(MyEnum:OFF)}"), "templates/foo.html")); @Inject @@ -33,10 +31,10 @@ public class MessageBundleLogicalLineTest { @Test public void testResolvers() { - assertEquals("Hello Edgar! Hello \n Edgar! ::apple, banana, pear, watermelon, kiwi, mango", + assertEquals("Hello Edgar!::Hello \n Edgar!::apple, banana, pear, watermelon, kiwi, mango::Off", foo.render()); - assertEquals("Ahoj Edgar a dobrý den! Ahoj \n Edgar! ::apple, banana, pear, watermelon, kiwi, mango", - foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render()); + assertEquals("Ahoj Edgar a dobrý den!::Ahoj \n Edgar!::jablko, banan, hruska, meloun, kiwi, mango::Vypnuto", + foo.instance().setLocale("cs").render()); } @MessageBundle(value = DEFAULT_NAME, locale = "en") @@ -50,6 +48,21 @@ public interface Messages { @Message("apple, banana, pear, watermelon, kiwi, mango") String fruits(); + + @Message("{#when myEnum}" + + "{#is ON}On" + + "{#is OFF}Off" + + "{#else}Undefined" + + "{/when}") + String myEnum(MyEnum myEnum); + + } + + @TemplateEnum + public enum MyEnum { + ON, + OFF, + UNDEFINED } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java index 3b998b0a02af6..6acf6738cb8ed 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java @@ -84,7 +84,7 @@ public void testResolvers() { assertEquals("Hello world! Ahoj Jachym! Hello you guys! Hello alpha! Hello! Hello foo from alpha!", foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render()); assertEquals("Hallo Welt! Hallo Jachym! Hello you guys! Hello alpha! Hello! Hello foo from alpha!", - foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.GERMAN).render()); + foo.instance().setLocale(Locale.GERMAN).render()); assertEquals("Dot test!", engine.parse("{msg:['dot.test']}").render()); assertEquals("Hello world! Hello Malachi Constant!", engine.getTemplate("dynamic").data("key", "hello_fullname").data("surname", "Constant").render()); diff --git a/extensions/qute/deployment/src/test/resources/messages/msg_cs.properties b/extensions/qute/deployment/src/test/resources/messages/msg_cs.properties index 4b54f8bf586b8..e322d21914f7d 100644 --- a/extensions/qute/deployment/src/test/resources/messages/msg_cs.properties +++ b/extensions/qute/deployment/src/test/resources/messages/msg_cs.properties @@ -3,7 +3,14 @@ hello=Ahoj \ dobrý den! helloNextLine=Ahoj \n {name}! - fruits = apple, banana, pear, \ - watermelon, \ + fruits = jablko, banan, hruska, \ + meloun, \ kiwi, mango + + # This is an example how to localize an enum value +myEnum={#when myEnum}\ + {#is ON}Zapnuto\ + {#is OFF}Vypnuto\ + {#else}Nedefinovano\ + {/when} \ No newline at end of file From af00b5f1a670f705492b92898ee6f80f4c57697a Mon Sep 17 00:00:00 2001 From: Antonio Musarra Date: Tue, 16 Apr 2024 14:18:03 +0200 Subject: [PATCH 56/72] Update deploying-to-openshift.adoc Replace the build option -Dquarkus.kubernetes.deploy=true with -Dquarkus.openshift.deploy=true --- docs/src/main/asciidoc/deploying-to-openshift.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/deploying-to-openshift.adoc b/docs/src/main/asciidoc/deploying-to-openshift.adoc index 09eb23d61ae75..b4500631fa757 100644 --- a/docs/src/main/asciidoc/deploying-to-openshift.adoc +++ b/docs/src/main/asciidoc/deploying-to-openshift.adoc @@ -86,7 +86,7 @@ You can trigger a build and deployment in a single step or build the container i To trigger a build and deployment in a single step: -:build-additional-parameters: -Dquarkus.kubernetes.deploy=true +:build-additional-parameters: -Dquarkus.openshift.deploy=true include::{includes}/devtools/build.adoc[] :!build-additional-parameters: From 9da5774331b11ab166b746d14f3717610e058422 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Thu, 11 Jan 2024 08:31:14 +0100 Subject: [PATCH 57/72] Incubating implementation of a more efficient ApplicationModel resolver for Maven projects --- .../quarkus/deployment/BootstrapConfig.java | 9 + .../CascadingConditionalDependenciesTest.java | 22 +- .../ConditionalDependencyScenarioTwoTest.java | 55 +- ...onalDependencyWithSingleConditionTest.java | 35 +- ...tionalDependencyWithTwoConditionsTest.java | 16 +- .../BootstrapFromOriginalJarTestBase.java | 76 +- .../DeploymentDependencyConvergenceTest.java | 68 + .../runnerjar/ReloadableFlagsTest.java | 71 + .../io/quarkus/maven/DependencyTreeMojo.java | 37 +- .../main/java/io/quarkus/maven/DevMojo.java | 2 + .../maven/QuarkusBootstrapProvider.java | 4 + .../maven/BasicDependencyTreeTestBase.java | 26 + .../ConditionalDependencyGraphMojoTest.java | 53 + .../ConditionalDependencyTreeMojoTest.java | 48 + .../maven/DependencyTreeMojoTestBase.java | 71 +- .../maven/DevDependencyTreeMojoTest.java | 2 +- .../maven/ProdDependencyTreeMojoTest.java | 2 +- .../maven/TestDependencyTreeMojoTest.java | 2 +- .../app-with-conditional-deps-1.jar.prod | 15 + .../app-with-conditional-graph-1.jar.prod | 30 + .../src/test/resources/test-app-1.jar.dev | 2 +- .../src/test/resources/test-app-1.jar.prod | 2 +- .../src/test/resources/test-app-1.jar.test | 2 +- .../model/ApplicationModelBuilder.java | 16 +- .../model/DefaultApplicationModel.java | 4 +- .../maven/dependency/DependencyFlags.java | 1 - .../resolver/CollectDependenciesBase.java | 3 +- .../resolver/ResolverSetupCleanup.java | 2 + .../bootstrap/resolver/TsArtifact.java | 12 +- .../resolver/BootstrapAppModelResolver.java | 57 +- .../ApplicationDependencyModelResolver.java | 1257 +++++++++++++++++ .../ApplicationDependencyTreeResolver.java | 3 +- .../maven/BootstrapModelResolver.java | 8 +- .../maven/DependencyLoggingConfig.java | 65 + .../bootstrap/util/DependencyUtils.java | 10 + 35 files changed, 1914 insertions(+), 174 deletions(-) create mode 100644 core/deployment/src/test/java/io/quarkus/deployment/runnerjar/DeploymentDependencyConvergenceTest.java create mode 100644 core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ReloadableFlagsTest.java create mode 100644 devtools/maven/src/test/java/io/quarkus/maven/BasicDependencyTreeTestBase.java create mode 100644 devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyGraphMojoTest.java create mode 100644 devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyTreeMojoTest.java create mode 100644 devtools/maven/src/test/resources/app-with-conditional-deps-1.jar.prod create mode 100644 devtools/maven/src/test/resources/app-with-conditional-graph-1.jar.prod create mode 100644 independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyModelResolver.java create mode 100644 independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/DependencyLoggingConfig.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java index 8f3e059be79f0..fc4e1e776034c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/BootstrapConfig.java @@ -41,6 +41,15 @@ public class BootstrapConfig { @ConfigItem(defaultValue = "false") boolean disableJarCache; + /** + * A temporary option introduced to avoid a logging warning when {@code }-Dquarkus.bootstrap.incubating-model-resolver} + * is added to the build command line. + * This option enables an incubating implementation of the Quarkus Application Model resolver. + * This option will be removed as soon as the incubating implementation becomes the default one. + */ + @ConfigItem(defaultValue = "false") + boolean incubatingModelResolver; + /** * Whether to throw an error, warn or silently ignore misaligned platform BOM imports */ diff --git a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/CascadingConditionalDependenciesTest.java b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/CascadingConditionalDependenciesTest.java index cb87d130aa054..4e668d0ca9cad 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/CascadingConditionalDependenciesTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/CascadingConditionalDependenciesTest.java @@ -5,14 +5,16 @@ import java.util.HashSet; import java.util.Set; +import org.eclipse.aether.util.artifact.JavaScopes; + import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.TsArtifact; import io.quarkus.bootstrap.resolver.TsQuarkusExt; import io.quarkus.deployment.runnerjar.BootstrapFromOriginalJarTestBase; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactDependency; import io.quarkus.maven.dependency.Dependency; import io.quarkus.maven.dependency.DependencyFlags; -import io.quarkus.maven.dependency.GACTV; public class CascadingConditionalDependenciesTest extends BootstrapFromOriginalJarTestBase { @@ -70,22 +72,28 @@ protected TsArtifact composeApplication() { protected void assertAppModel(ApplicationModel model) throws Exception { final Set expected = new HashSet<>(); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-d-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-d-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-e-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-e-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-f-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-f-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); assertEquals(expected, getDeploymentOnlyDeps(model)); } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyScenarioTwoTest.java b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyScenarioTwoTest.java index 4d4bc3f0881b7..a55543f9c1710 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyScenarioTwoTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyScenarioTwoTest.java @@ -4,16 +4,17 @@ import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; + +import org.eclipse.aether.util.artifact.JavaScopes; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.TsArtifact; import io.quarkus.bootstrap.resolver.TsQuarkusExt; import io.quarkus.deployment.runnerjar.BootstrapFromOriginalJarTestBase; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactDependency; import io.quarkus.maven.dependency.Dependency; import io.quarkus.maven.dependency.DependencyFlags; -import io.quarkus.maven.dependency.GACTV; public class ConditionalDependencyScenarioTwoTest extends BootstrapFromOriginalJarTestBase { @@ -114,54 +115,68 @@ protected TsArtifact composeApplication() { @Override protected void assertAppModel(ApplicationModel appModel) throws Exception { - final Set deploymentDeps = appModel.getDependencies().stream() - .filter(d -> d.isDeploymentCp() && !d.isRuntimeCp()).map(d -> new ArtifactDependency(d)) - .collect(Collectors.toSet()); + var deploymentDeps = getDeploymentOnlyDeps(appModel); + ; final Set expected = new HashSet<>(); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-f-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-f-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-g-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-g-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-h-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-h-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-k-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-k-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-l-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-l-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-j-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-j-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-m-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-m-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-n-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-n-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-i-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-i-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-o-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-o-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-p-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-p-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-r-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-r-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-s-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-s-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-t-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-t-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-u-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-u-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); assertEquals(expected, new HashSet<>(deploymentDeps)); } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithSingleConditionTest.java b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithSingleConditionTest.java index a26d42a488b7f..bcd424cd8cbf3 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithSingleConditionTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithSingleConditionTest.java @@ -2,18 +2,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; + +import org.eclipse.aether.util.artifact.JavaScopes; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.TsArtifact; import io.quarkus.bootstrap.resolver.TsQuarkusExt; import io.quarkus.deployment.runnerjar.BootstrapFromOriginalJarTestBase; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactDependency; -import io.quarkus.maven.dependency.Dependency; import io.quarkus.maven.dependency.DependencyFlags; -import io.quarkus.maven.dependency.GACTV; public class ConditionalDependencyWithSingleConditionTest extends BootstrapFromOriginalJarTestBase { @@ -42,19 +41,19 @@ protected TsArtifact composeApplication() { @Override protected void assertAppModel(ApplicationModel appModel) throws Exception { - final Set deploymentDeps = appModel.getDependencies().stream() - .filter(d -> d.isDeploymentCp() && !d.isRuntimeCp()).map(d -> new ArtifactDependency(d)) - .collect(Collectors.toSet()); - final Set expected = new HashSet<>(); - expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), "compile", - DependencyFlags.DEPLOYMENT_CP)); - expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), "compile", - DependencyFlags.DEPLOYMENT_CP)); - expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), "runtime", - DependencyFlags.DEPLOYMENT_CP)); - assertEquals(expected, deploymentDeps); + var expected = Set.of( + new ArtifactDependency( + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency( + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency( + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP)); + assertEquals(expected, getDeploymentOnlyDeps(appModel)); } } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithTwoConditionsTest.java b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithTwoConditionsTest.java index 9f73397e60e3d..aa6a14336d556 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithTwoConditionsTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/ConditionalDependencyWithTwoConditionsTest.java @@ -5,14 +5,16 @@ import java.util.HashSet; import java.util.Set; +import org.eclipse.aether.util.artifact.JavaScopes; + import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.TsArtifact; import io.quarkus.bootstrap.resolver.TsQuarkusExt; import io.quarkus.deployment.runnerjar.BootstrapFromOriginalJarTestBase; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactDependency; import io.quarkus.maven.dependency.Dependency; import io.quarkus.maven.dependency.DependencyFlags; -import io.quarkus.maven.dependency.GACTV; public class ConditionalDependencyWithTwoConditionsTest extends BootstrapFromOriginalJarTestBase { @@ -46,16 +48,20 @@ protected TsArtifact composeApplication() { protected void assertAppModel(ApplicationModel model) throws Exception { final Set expected = new HashSet<>(); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-c-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-a-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), "runtime", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-b-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); expected.add(new ArtifactDependency( - new GACTV(TsArtifact.DEFAULT_GROUP_ID, "ext-d-deployment", TsArtifact.DEFAULT_VERSION), "compile", + ArtifactCoords.jar(TsArtifact.DEFAULT_GROUP_ID, "ext-d-deployment", TsArtifact.DEFAULT_VERSION), + JavaScopes.COMPILE, DependencyFlags.DEPLOYMENT_CP)); assertEquals(expected, getDeploymentOnlyDeps(model)); } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/BootstrapFromOriginalJarTestBase.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/BootstrapFromOriginalJarTestBase.java index 0b7a314795338..d9886d3bdd8e0 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/BootstrapFromOriginalJarTestBase.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/BootstrapFromOriginalJarTestBase.java @@ -75,24 +75,27 @@ protected QuarkusBootstrap.Builder initBootstrapBuilder() throws Exception { .setAppModelResolver(resolver) .setTest(isBootstrapForTestMode()); - if (createWorkspace()) { + if (createWorkspace() || !wsModules.isEmpty()) { System.setProperty("basedir", ws.toAbsolutePath().toString()); final Model appPom = appJar.getPomModel(); - final List bomModules = (appPom.getDependencyManagement() == null ? List. of() - : appPom.getDependencyManagement().getDependencies()).stream() - .filter(d -> "import".equals(d.getScope()) - && d.getGroupId().equals(appPom.getGroupId())) - .collect(Collectors.toList()); - - final List depModules = appPom.getDependencies().stream() - .filter(d -> d.getGroupId().equals(appPom.getGroupId()) && - (d.getType().isEmpty() || ArtifactCoords.TYPE_JAR.equals(d.getType()))) - .collect(Collectors.toList()); + List bomModules = List.of(); + List depModules = List.of(); + if (createWorkspace()) { + bomModules = (appPom.getDependencyManagement() == null ? List. of() + : appPom.getDependencyManagement().getDependencies()).stream() + .filter(d -> "import".equals(d.getScope()) + && d.getGroupId().equals(appPom.getGroupId())) + .collect(Collectors.toList()); + depModules = appPom.getDependencies().stream() + .filter(d -> d.getGroupId().equals(appPom.getGroupId()) && + (d.getType().isEmpty() || ArtifactCoords.TYPE_JAR.equals(d.getType()))) + .collect(Collectors.toList()); + } final Path appModule; final Path appPomXml; - if (depModules.isEmpty() && bomModules.isEmpty() || appPom.getParent() != null) { + if (depModules.isEmpty() && bomModules.isEmpty() && wsModules.isEmpty() || appPom.getParent() != null) { appModule = ws; appPomXml = ws.resolve("pom.xml"); ModelUtils.persistModel(appPomXml, appPom); @@ -130,31 +133,32 @@ protected QuarkusBootstrap.Builder initBootstrapBuilder() throws Exception { ModelUtils.persistModel(appPomXml, appPom); // dependency modules - final Map managedVersions = new HashMap<>(); - collectManagedDeps(appPom, managedVersions); - for (Dependency moduleDep : depModules) { - parentPom.getModules().add(moduleDep.getArtifactId()); - final String moduleVersion = moduleDep.getVersion() == null - ? managedVersions.get(ArtifactKey.of(moduleDep.getGroupId(), moduleDep.getArtifactId(), - moduleDep.getClassifier(), moduleDep.getType())) - : moduleDep.getVersion(); - Model modulePom = ModelUtils.readModel(resolver - .resolve(ArtifactCoords.pom(moduleDep.getGroupId(), moduleDep.getArtifactId(), moduleVersion)) - .getResolvedPaths().getSinglePath()); - modulePom.setParent(parent); - final Path moduleDir = IoUtils.mkdirs(ws.resolve(modulePom.getArtifactId())); - ModelUtils.persistModel(moduleDir.resolve("pom.xml"), modulePom); - final Path resolvedJar = resolver - .resolve(ArtifactCoords.of(modulePom.getGroupId(), modulePom.getArtifactId(), - moduleDep.getClassifier(), moduleDep.getType(), modulePom.getVersion())) - .getResolvedPaths() - .getSinglePath(); - final Path moduleTargetDir = moduleDir.resolve("target"); - ZipUtils.unzip(resolvedJar, moduleTargetDir.resolve("classes")); - IoUtils.copy(resolvedJar, - moduleTargetDir.resolve(modulePom.getArtifactId() + "-" + modulePom.getVersion() + ".jar")); + if (!depModules.isEmpty()) { + final Map managedVersions = new HashMap<>(); + collectManagedDeps(appPom, managedVersions); + for (Dependency moduleDep : depModules) { + parentPom.getModules().add(moduleDep.getArtifactId()); + final String moduleVersion = moduleDep.getVersion() == null + ? managedVersions.get(ArtifactKey.of(moduleDep.getGroupId(), moduleDep.getArtifactId(), + moduleDep.getClassifier(), moduleDep.getType())) + : moduleDep.getVersion(); + Model modulePom = ModelUtils.readModel(resolver + .resolve(ArtifactCoords.pom(moduleDep.getGroupId(), moduleDep.getArtifactId(), moduleVersion)) + .getResolvedPaths().getSinglePath()); + modulePom.setParent(parent); + final Path moduleDir = IoUtils.mkdirs(ws.resolve(modulePom.getArtifactId())); + ModelUtils.persistModel(moduleDir.resolve("pom.xml"), modulePom); + final Path resolvedJar = resolver + .resolve(ArtifactCoords.of(modulePom.getGroupId(), modulePom.getArtifactId(), + moduleDep.getClassifier(), moduleDep.getType(), modulePom.getVersion())) + .getResolvedPaths() + .getSinglePath(); + final Path moduleTargetDir = moduleDir.resolve("target"); + ZipUtils.unzip(resolvedJar, moduleTargetDir.resolve("classes")); + IoUtils.copy(resolvedJar, + moduleTargetDir.resolve(modulePom.getArtifactId() + "-" + modulePom.getVersion() + ".jar")); + } } - for (TsArtifact module : wsModules) { parentPom.getModules().add(module.getArtifactId()); Model modulePom = module.getPomModel(); diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/DeploymentDependencyConvergenceTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/DeploymentDependencyConvergenceTest.java new file mode 100644 index 0000000000000..77d6c52bee6c1 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/DeploymentDependencyConvergenceTest.java @@ -0,0 +1,68 @@ +package io.quarkus.deployment.runnerjar; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.eclipse.aether.util.artifact.JavaScopes; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; +import io.quarkus.maven.dependency.*; + +public class DeploymentDependencyConvergenceTest extends BootstrapFromOriginalJarTestBase { + + @Override + protected TsArtifact composeApplication() { + + var libE10 = install(TsArtifact.jar("lib-e", "1.0")); + var libE20 = install(TsArtifact.jar("lib-e", "2.0")); + var libE30 = install(TsArtifact.jar("lib-e", "3.0")); + + var libD10 = install(TsArtifact.jar("lib-d", "1.0")); + var libD20 = install(TsArtifact.jar("lib-d", "2.0")); + + var libC10 = install(TsArtifact.jar("lib-c", "1.0") + .addDependency(libD10) + .addDependency(libE10)); + + var libB10 = install(TsArtifact.jar("lib-b", "1.0")); + var libB20 = install(TsArtifact.jar("lib-b", "2.0") + .addDependency(libC10)); + + var extA = new TsQuarkusExt("ext-a"); + addToExpectedLib(extA.getRuntime()); + extA.getDeployment() + .addManagedDependency(libD20) + .addManagedDependency(libE20) + .addDependency(libB10); + + return TsArtifact.jar("app") + .addManagedDependency(platformDescriptor()) + .addManagedDependency(platformProperties()) + .addManagedDependency(libB20) + .addManagedDependency(libE30) + .addDependency(extA); + } + + @Override + protected void assertAppModel(ApplicationModel model) throws Exception { + final Set expected = Set.of( + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a", "1"), JavaScopes.COMPILE, + DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP, DependencyFlags.DIRECT, + DependencyFlags.RUNTIME_EXTENSION_ARTIFACT, DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "ext-a-deployment", "1"), + JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "lib-b", "2.0"), JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "lib-c", "1.0"), JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "lib-d", "2.0"), JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP), + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "lib-e", "3.0"), JavaScopes.COMPILE, + DependencyFlags.DEPLOYMENT_CP)); + assertEquals(expected, getDependenciesWithFlag(model, DependencyFlags.DEPLOYMENT_CP)); + } +} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ReloadableFlagsTest.java b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ReloadableFlagsTest.java new file mode 100644 index 0000000000000..5614d62716483 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/runnerjar/ReloadableFlagsTest.java @@ -0,0 +1,71 @@ +package io.quarkus.deployment.runnerjar; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.aether.util.artifact.JavaScopes; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; +import io.quarkus.bootstrap.workspace.WorkspaceModule; +import io.quarkus.bootstrap.workspace.WorkspaceModuleId; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactDependency; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.maven.dependency.DependencyFlags; + +public class ReloadableFlagsTest extends BootstrapFromOriginalJarTestBase { + + @Override + protected TsArtifact composeApplication() { + + var transitive = TsArtifact.jar("acme-transitive"); + addWorkspaceModule(transitive); + addToExpectedLib(transitive); + + var common = TsArtifact.jar("acme-common"); + common.addDependency(transitive); + addWorkspaceModule(common); + addToExpectedLib(common); + + var lib = TsArtifact.jar("acme-lib"); + lib.addDependency(common); + addWorkspaceModule(lib); + addToExpectedLib(lib); + + var externalLib = TsArtifact.jar("external-lib"); + externalLib.addDependency(common); + addToExpectedLib(externalLib); + + var myExt = new TsQuarkusExt("my-ext"); + addToExpectedLib(myExt.getRuntime()); + + return TsArtifact.jar("app") + .addManagedDependency(platformDescriptor()) + .addManagedDependency(platformProperties()) + .addDependency(common) + .addDependency(lib) + .addDependency(externalLib) + .addDependency(myExt); + } + + @Override + protected void assertAppModel(ApplicationModel model) { + assertThat(model.getWorkspaceModules().stream().map(WorkspaceModule::getId).collect(Collectors.toSet())) + .isEqualTo(Set.of( + WorkspaceModuleId.of(TsArtifact.DEFAULT_GROUP_ID, "acme-transitive", TsArtifact.DEFAULT_VERSION), + WorkspaceModuleId.of(TsArtifact.DEFAULT_GROUP_ID, "acme-common", TsArtifact.DEFAULT_VERSION), + WorkspaceModuleId.of(TsArtifact.DEFAULT_GROUP_ID, "acme-lib", TsArtifact.DEFAULT_VERSION), + WorkspaceModuleId.of(TsArtifact.DEFAULT_GROUP_ID, "app", TsArtifact.DEFAULT_VERSION))); + + final Set expected = Set.of( + new ArtifactDependency(ArtifactCoords.jar("io.quarkus.bootstrap.test", "acme-lib", "1"), JavaScopes.COMPILE, + DependencyFlags.RUNTIME_CP, DependencyFlags.DEPLOYMENT_CP, DependencyFlags.DIRECT, + DependencyFlags.RELOADABLE, DependencyFlags.WORKSPACE_MODULE)); + + assertThat(getDependenciesWithFlag(model, DependencyFlags.RELOADABLE)).isEqualTo(expected); + } +} diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java index 22891e8f44de4..6e027daff769d 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; import java.util.List; import java.util.function.Consumer; @@ -21,7 +22,9 @@ import org.eclipse.aether.repository.RemoteRepository; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; +import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; +import io.quarkus.bootstrap.resolver.maven.DependencyLoggingConfig; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.maven.components.QuarkusWorkspaceProvider; import io.quarkus.maven.dependency.ArtifactCoords; @@ -48,9 +51,27 @@ public class DependencyTreeMojo extends AbstractMojo { * Target launch mode corresponding to {@link io.quarkus.runtime.LaunchMode} for which the dependency tree should be built. * {@code io.quarkus.runtime.LaunchMode.NORMAL} is the default. */ - @Parameter(property = "mode", required = false, defaultValue = "prod") + @Parameter(property = "mode", defaultValue = "prod") String mode; + /** + * INCUBATING option, enabled with @{code -Dquarkus.bootstrap.incubating-model-resolver} system or project property. + *

+ * Whether to log dependency properties, such as on which classpath they belong, whether they are hot-reloadable in dev + * mode, etc. + */ + @Parameter(property = "verbose") + boolean verbose; + + /** + * INCUBATING option, enabled with @{code -Dquarkus.bootstrap.incubating-model-resolver} system or project property. + *

+ * Whether to log all dependencies of each dependency node in a tree, adding {@code [+]} suffix + * to those whose dependencies are not expanded. + */ + @Parameter(property = "graph") + boolean graph; + /** * If specified, this parameter will cause the dependency tree to be written to the path specified, instead of writing to * the console. @@ -77,8 +98,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { final BufferedWriter bw; try { Files.createDirectories(outputFile.toPath().getParent()); - bw = writer = Files.newBufferedWriter(outputFile.toPath(), - appendOutput && outputFile.exists() ? StandardOpenOption.APPEND : StandardOpenOption.CREATE); + final OpenOption[] openOptions = appendOutput && outputFile.exists() + ? new OpenOption[] { StandardOpenOption.APPEND } + : new OpenOption[0]; + bw = writer = Files.newBufferedWriter(outputFile.toPath(), openOptions); } catch (IOException e) { throw new MojoExecutionException("Failed to initialize file output writer", e); } @@ -124,7 +147,13 @@ private void logTree(final Consumer log) throws MojoExecutionException { "Parameter 'mode' was set to '" + mode + "' while expected one of 'dev', 'test' or 'prod'"); } } - modelResolver.setBuildTreeLogger(log); + modelResolver.setIncubatingModelResolver( + ApplicationDependencyModelResolver.isIncubatingEnabled(project.getProperties())); + modelResolver.setDepLogConfig(DependencyLoggingConfig.builder() + .setMessageConsumer(log) + .setVerbose(verbose) + .setGraph(graph) + .build()); modelResolver.resolveModel(appArtifact); } catch (Exception e) { throw new MojoExecutionException("Failed to resolve application model " + appArtifact + " dependencies", e); diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index 9dc4a552aad8c..05bd7d3b81622 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -96,6 +96,7 @@ import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.model.PathsCollection; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; +import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContextConfig; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; @@ -1360,6 +1361,7 @@ private QuarkusDevModeLauncher newLauncher(Boolean debugPortOk, String bootstrap .setDevMode(true) .setTest(LaunchMode.TEST.equals(getLaunchModeClasspath())) .setCollectReloadableDependencies(!noDeps) + .setIncubatingModelResolver(ApplicationDependencyModelResolver.isIncubatingEnabled(project.getProperties())) .resolveModel(mvnCtx.getCurrentProject().getAppArtifact()); } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java index 1d8f7559b2fb2..d83cb4fa9f46b 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java @@ -37,6 +37,7 @@ import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; +import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; @@ -204,7 +205,10 @@ private MavenArtifactResolver artifactResolver(QuarkusBootstrapMojo mojo, Launch private CuratedApplication doBootstrap(QuarkusBootstrapMojo mojo, LaunchMode mode) throws MojoExecutionException { + final BootstrapAppModelResolver modelResolver = new BootstrapAppModelResolver(artifactResolver(mojo, mode)) + .setIncubatingModelResolver( + ApplicationDependencyModelResolver.isIncubatingEnabled(mojo.mavenProject().getProperties())) .setDevMode(mode == LaunchMode.DEVELOPMENT) .setTest(mode == LaunchMode.TEST) .setCollectReloadableDependencies(mode == LaunchMode.DEVELOPMENT || mode == LaunchMode.TEST); diff --git a/devtools/maven/src/test/java/io/quarkus/maven/BasicDependencyTreeTestBase.java b/devtools/maven/src/test/java/io/quarkus/maven/BasicDependencyTreeTestBase.java new file mode 100644 index 0000000000000..ef929a5567a5e --- /dev/null +++ b/devtools/maven/src/test/java/io/quarkus/maven/BasicDependencyTreeTestBase.java @@ -0,0 +1,26 @@ +package io.quarkus.maven; + +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsDependency; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; + +abstract class BasicDependencyTreeTestBase extends DependencyTreeMojoTestBase { + + @Override + protected void initRepo() { + + final TsQuarkusExt coreExt = new TsQuarkusExt("test-core-ext"); + app = TsArtifact.jar("test-app") + .addDependency(new TsArtifact(TsArtifact.DEFAULT_GROUP_ID, "artifact-with-classifier", "classifier", "jar", + TsArtifact.DEFAULT_VERSION)) + .addDependency(new TsQuarkusExt("test-ext2") + .addDependency(new TsQuarkusExt("test-ext1").addDependency(coreExt))) + .addDependency(new TsDependency(TsArtifact.jar("optional"), true)) + .addDependency(new TsQuarkusExt("test-ext3").addDependency(coreExt)) + .addDependency(new TsDependency(TsArtifact.jar("provided"), "provided")) + .addDependency(new TsDependency(TsArtifact.jar("runtime"), "runtime")) + .addDependency(new TsDependency(TsArtifact.jar("test"), "test")); + appModel = app.getPomModel(); + app.install(repoBuilder); + } +} diff --git a/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyGraphMojoTest.java b/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyGraphMojoTest.java new file mode 100644 index 0000000000000..26509fd19828f --- /dev/null +++ b/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyGraphMojoTest.java @@ -0,0 +1,53 @@ +package io.quarkus.maven; + +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; + +public class ConditionalDependencyGraphMojoTest extends DependencyTreeMojoTestBase { + @Override + protected String mode() { + return "prod"; + } + + @Override + protected boolean isGraph() { + return true; + } + + @Override + protected boolean isIncubatingModelResolver() { + return true; + } + + @Override + protected void initRepo() { + + final TsQuarkusExt coreExt = new TsQuarkusExt("test-core-ext"); + + var tomatoExt = new TsQuarkusExt("quarkus-tomato").addDependency(coreExt); + var mozzarellaExt = new TsQuarkusExt("quarkus-mozzarella").addDependency(coreExt); + var basilExt = new TsQuarkusExt("quarkus-basil").addDependency(coreExt); + + var oilJar = TsArtifact.jar("quarkus-oil"); + + var capreseExt = new TsQuarkusExt("quarkus-caprese") + .setDependencyCondition(tomatoExt, mozzarellaExt, basilExt) + .addDependency(coreExt); + capreseExt.getDeployment().addDependency(oilJar); + capreseExt.install(repoBuilder); + + var saladExt = new TsQuarkusExt("quarkus-salad") + .setConditionalDeps(capreseExt) + .addDependency(coreExt); + + app = TsArtifact.jar("app-with-conditional-graph") + .addDependency(tomatoExt) + .addDependency(mozzarellaExt) + .addDependency(basilExt) + .addDependency(saladExt) + .addDependency(oilJar); + + appModel = app.getPomModel(); + app.install(repoBuilder); + } +} diff --git a/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyTreeMojoTest.java b/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyTreeMojoTest.java new file mode 100644 index 0000000000000..45d10a0417b62 --- /dev/null +++ b/devtools/maven/src/test/java/io/quarkus/maven/ConditionalDependencyTreeMojoTest.java @@ -0,0 +1,48 @@ +package io.quarkus.maven; + +import io.quarkus.bootstrap.resolver.TsArtifact; +import io.quarkus.bootstrap.resolver.TsQuarkusExt; + +public class ConditionalDependencyTreeMojoTest extends DependencyTreeMojoTestBase { + @Override + protected String mode() { + return "prod"; + } + + @Override + protected boolean isIncubatingModelResolver() { + return true; + } + + @Override + protected void initRepo() { + + final TsQuarkusExt coreExt = new TsQuarkusExt("test-core-ext"); + + var tomatoExt = new TsQuarkusExt("quarkus-tomato").addDependency(coreExt); + var mozzarellaExt = new TsQuarkusExt("quarkus-mozzarella").addDependency(coreExt); + var basilExt = new TsQuarkusExt("quarkus-basil").addDependency(coreExt); + + var oilJar = TsArtifact.jar("quarkus-oil"); + + var capreseExt = new TsQuarkusExt("quarkus-caprese") + .setDependencyCondition(tomatoExt, mozzarellaExt, basilExt) + .addDependency(coreExt); + capreseExt.getDeployment().addDependency(oilJar); + capreseExt.install(repoBuilder); + + var saladExt = new TsQuarkusExt("quarkus-salad") + .setConditionalDeps(capreseExt) + .addDependency(coreExt); + + app = TsArtifact.jar("app-with-conditional-deps") + .addDependency(tomatoExt) + .addDependency(mozzarellaExt) + .addDependency(basilExt) + .addDependency(saladExt) + .addDependency(oilJar); + + appModel = app.getPomModel(); + app.install(repoBuilder); + } +} diff --git a/devtools/maven/src/test/java/io/quarkus/maven/DependencyTreeMojoTestBase.java b/devtools/maven/src/test/java/io/quarkus/maven/DependencyTreeMojoTestBase.java index bd99f28420ec7..db3a359c1d6d3 100644 --- a/devtools/maven/src/test/java/io/quarkus/maven/DependencyTreeMojoTestBase.java +++ b/devtools/maven/src/test/java/io/quarkus/maven/DependencyTreeMojoTestBase.java @@ -1,32 +1,28 @@ package io.quarkus.maven; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; -import java.io.BufferedReader; -import java.io.IOException; import java.io.PrintStream; -import java.nio.file.Files; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; import org.apache.maven.artifact.DefaultArtifact; import org.apache.maven.artifact.handler.DefaultArtifactHandler; import org.apache.maven.model.Model; import org.apache.maven.project.MavenProject; +import org.eclipse.aether.util.artifact.JavaScopes; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import io.quarkus.bootstrap.resolver.TsArtifact; -import io.quarkus.bootstrap.resolver.TsDependency; -import io.quarkus.bootstrap.resolver.TsQuarkusExt; import io.quarkus.bootstrap.resolver.TsRepoBuilder; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.bootstrap.util.IoUtils; +import io.quarkus.maven.dependency.ArtifactCoords; -public abstract class DependencyTreeMojoTestBase { +abstract class DependencyTreeMojoTestBase { protected Path workDir; protected Path repoHome; @@ -50,22 +46,6 @@ public void setup() throws Exception { initRepo(); } - protected void initRepo() throws Exception { - final TsQuarkusExt coreExt = new TsQuarkusExt("test-core-ext"); - app = TsArtifact.jar("test-app") - .addDependency(new TsArtifact(TsArtifact.DEFAULT_GROUP_ID, "artifact-with-classifier", "classifier", "jar", - TsArtifact.DEFAULT_VERSION)) - .addDependency(new TsQuarkusExt("test-ext2") - .addDependency(new TsQuarkusExt("test-ext1").addDependency(coreExt))) - .addDependency(new TsDependency(TsArtifact.jar("optional"), true)) - .addDependency(new TsQuarkusExt("test-ext3").addDependency(coreExt)) - .addDependency(new TsDependency(TsArtifact.jar("provided"), "provided")) - .addDependency(new TsDependency(TsArtifact.jar("runtime"), "runtime")) - .addDependency(new TsDependency(TsArtifact.jar("test"), "test")); - appModel = app.getPomModel(); - app.install(repoBuilder); - } - @AfterEach public void cleanup() { if (workDir != null) { @@ -73,44 +53,47 @@ public void cleanup() { } } + protected abstract void initRepo(); + protected abstract String mode(); + protected boolean isGraph() { + return false; + } + + protected boolean isIncubatingModelResolver() { + return false; + } + @Test public void test() throws Exception { final DependencyTreeMojo mojo = new DependencyTreeMojo(); mojo.project = new MavenProject(); - mojo.project.setArtifact(new DefaultArtifact(app.getGroupId(), app.getArtifactId(), app.getVersion(), "compile", - app.getType(), app.getClassifier(), new DefaultArtifactHandler("jar"))); + mojo.project.setArtifact(new DefaultArtifact(app.getGroupId(), app.getArtifactId(), app.getVersion(), + JavaScopes.COMPILE, app.getType(), app.getClassifier(), + new DefaultArtifactHandler(ArtifactCoords.TYPE_JAR))); mojo.project.setModel(appModel); mojo.project.setOriginalModel(appModel); + if (isIncubatingModelResolver()) { + mojo.project.getProperties().setProperty("quarkus.bootstrap.incubating-model-resolver", "true"); + } mojo.resolver = mvnResolver; mojo.mode = mode(); + mojo.graph = isGraph(); - final Path mojoLog = workDir.resolve("mojo.log"); + final Path mojoLog = workDir.resolve(getClass().getName() + ".log"); final PrintStream defaultOut = System.out; - try (PrintStream logOut = new PrintStream(mojoLog.toFile(), "UTF-8")) { + try (PrintStream logOut = new PrintStream(mojoLog.toFile(), StandardCharsets.UTF_8)) { System.setOut(logOut); mojo.execute(); } finally { System.setOut(defaultOut); } - assertEquals(readInLowCase(Path.of("").normalize().toAbsolutePath() - .resolve("target").resolve("test-classes") - .resolve(app.getArtifactFileName() + "." + mode())), readInLowCase(mojoLog)); - } - - private static List readInLowCase(Path p) throws IOException { - final List list = new ArrayList<>(); - try (BufferedReader reader = Files.newBufferedReader(p)) { - String line = reader.readLine(); - while (line != null) { - list.add(line.toLowerCase()); - line = reader.readLine(); - } - } - return list; + assertThat(mojoLog).hasSameTextualContentAs( + Path.of("").normalize().toAbsolutePath() + .resolve("target").resolve("test-classes").resolve(app.getArtifactFileName() + "." + mode())); } } diff --git a/devtools/maven/src/test/java/io/quarkus/maven/DevDependencyTreeMojoTest.java b/devtools/maven/src/test/java/io/quarkus/maven/DevDependencyTreeMojoTest.java index bef26188da25e..5dd5c52df7412 100644 --- a/devtools/maven/src/test/java/io/quarkus/maven/DevDependencyTreeMojoTest.java +++ b/devtools/maven/src/test/java/io/quarkus/maven/DevDependencyTreeMojoTest.java @@ -1,6 +1,6 @@ package io.quarkus.maven; -public class DevDependencyTreeMojoTest extends DependencyTreeMojoTestBase { +public class DevDependencyTreeMojoTest extends BasicDependencyTreeTestBase { @Override protected String mode() { return "dev"; diff --git a/devtools/maven/src/test/java/io/quarkus/maven/ProdDependencyTreeMojoTest.java b/devtools/maven/src/test/java/io/quarkus/maven/ProdDependencyTreeMojoTest.java index 81aa3c190b258..7e93473d3dc1b 100644 --- a/devtools/maven/src/test/java/io/quarkus/maven/ProdDependencyTreeMojoTest.java +++ b/devtools/maven/src/test/java/io/quarkus/maven/ProdDependencyTreeMojoTest.java @@ -1,6 +1,6 @@ package io.quarkus.maven; -public class ProdDependencyTreeMojoTest extends DependencyTreeMojoTestBase { +public class ProdDependencyTreeMojoTest extends BasicDependencyTreeTestBase { @Override protected String mode() { return "prod"; diff --git a/devtools/maven/src/test/java/io/quarkus/maven/TestDependencyTreeMojoTest.java b/devtools/maven/src/test/java/io/quarkus/maven/TestDependencyTreeMojoTest.java index fab83b72936e5..4ee403ebf5cf5 100644 --- a/devtools/maven/src/test/java/io/quarkus/maven/TestDependencyTreeMojoTest.java +++ b/devtools/maven/src/test/java/io/quarkus/maven/TestDependencyTreeMojoTest.java @@ -1,6 +1,6 @@ package io.quarkus.maven; -public class TestDependencyTreeMojoTest extends DependencyTreeMojoTestBase { +public class TestDependencyTreeMojoTest extends BasicDependencyTreeTestBase { @Override protected String mode() { return "test"; diff --git a/devtools/maven/src/test/resources/app-with-conditional-deps-1.jar.prod b/devtools/maven/src/test/resources/app-with-conditional-deps-1.jar.prod new file mode 100644 index 0000000000000..5780caf8fafca --- /dev/null +++ b/devtools/maven/src/test/resources/app-with-conditional-deps-1.jar.prod @@ -0,0 +1,15 @@ +[info] Quarkus application PROD mode build dependency tree: +[info] io.quarkus.bootstrap.test:app-with-conditional-deps:pom:1 +[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-tomato:jar:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:jar:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella-deployment:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:quarkus-mozzarella:jar:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:quarkus-basil-deployment:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:quarkus-basil:jar:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:quarkus-salad-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-salad:jar:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:quarkus-caprese:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:quarkus-caprese-deployment:jar:1 (compile) +[info] └─ io.quarkus.bootstrap.test:quarkus-oil:jar:1 (compile) \ No newline at end of file diff --git a/devtools/maven/src/test/resources/app-with-conditional-graph-1.jar.prod b/devtools/maven/src/test/resources/app-with-conditional-graph-1.jar.prod new file mode 100644 index 0000000000000..77508e6965d59 --- /dev/null +++ b/devtools/maven/src/test/resources/app-with-conditional-graph-1.jar.prod @@ -0,0 +1,30 @@ +[info] Quarkus application PROD mode build dependency tree: +[info] io.quarkus.bootstrap.test:app-with-conditional-graph:pom:1 +[info] ├─ io.quarkus.bootstrap.test:quarkus-basil::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-salad::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-tomato:jar:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:quarkus-mozzarella:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-basil-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:quarkus-basil:jar:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-salad-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-salad:jar:1 (compile) +[info] │ │ ├─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] +[info] │ │ └─ io.quarkus.bootstrap.test:quarkus-caprese:jar:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:quarkus-caprese-deployment:jar:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-caprese::jar:1 (compile) [+] +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-oil::jar:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] +[info] └─ io.quarkus.bootstrap.test:quarkus-oil:jar:1 (compile) \ No newline at end of file diff --git a/devtools/maven/src/test/resources/test-app-1.jar.dev b/devtools/maven/src/test/resources/test-app-1.jar.dev index 1fb959fdae99b..d43162a11db94 100644 --- a/devtools/maven/src/test/resources/test-app-1.jar.dev +++ b/devtools/maven/src/test/resources/test-app-1.jar.dev @@ -1,4 +1,4 @@ -[info] quarkus application dev mode build dependency tree: +[info] Quarkus application DEV mode build dependency tree: [info] io.quarkus.bootstrap.test:test-app:pom:1 [info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:jar:classifier:1 (compile) [info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:jar:1 (compile) diff --git a/devtools/maven/src/test/resources/test-app-1.jar.prod b/devtools/maven/src/test/resources/test-app-1.jar.prod index 5c460135a1273..bf7d0a9836aae 100644 --- a/devtools/maven/src/test/resources/test-app-1.jar.prod +++ b/devtools/maven/src/test/resources/test-app-1.jar.prod @@ -1,4 +1,4 @@ -[info] quarkus application prod mode build dependency tree: +[info] Quarkus application PROD mode build dependency tree: [info] io.quarkus.bootstrap.test:test-app:pom:1 [info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:jar:classifier:1 (compile) [info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:jar:1 (compile) diff --git a/devtools/maven/src/test/resources/test-app-1.jar.test b/devtools/maven/src/test/resources/test-app-1.jar.test index 40bc76f40c28c..832397770b902 100644 --- a/devtools/maven/src/test/resources/test-app-1.jar.test +++ b/devtools/maven/src/test/resources/test-app-1.jar.test @@ -1,4 +1,4 @@ -[info] quarkus application test mode build dependency tree: +[info] Quarkus application TEST mode build dependency tree: [info] io.quarkus.bootstrap.test:test-app:pom:1 [info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:jar:classifier:1 (compile) [info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:jar:1 (compile) diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java index 0dcc5c24da325..95784e1755be8 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java @@ -10,6 +10,8 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; import org.jboss.logging.Logger; @@ -36,13 +38,13 @@ public class ApplicationModelBuilder { ResolvedDependency appArtifact; final Map dependencies = new LinkedHashMap<>(); - final Set parentFirstArtifacts = new HashSet<>(); - final Set runnerParentFirstArtifacts = new HashSet<>(); - final List excludedArtifacts = new ArrayList<>(); - final Map> excludedResources = new HashMap<>(0); - final Set lesserPriorityArtifacts = new HashSet<>(); - final Set reloadableWorkspaceModules = new HashSet<>(); - final List extensionCapabilities = new ArrayList<>(); + final Collection parentFirstArtifacts = new ConcurrentLinkedDeque<>(); + final Collection runnerParentFirstArtifacts = new ConcurrentLinkedDeque<>(); + final Collection excludedArtifacts = new ConcurrentLinkedDeque<>(); + final Map> excludedResources = new ConcurrentHashMap<>(); + final Collection lesserPriorityArtifacts = new ConcurrentLinkedDeque<>(); + final Collection reloadableWorkspaceModules = new ConcurrentLinkedDeque<>(); + final Collection extensionCapabilities = new ConcurrentLinkedDeque<>(); PlatformImports platformImports; final Map projectModules = new HashMap<>(); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java index d245c1cad796c..b03ebc134dbcb 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java @@ -29,8 +29,8 @@ public DefaultApplicationModel(ApplicationModelBuilder builder) { this.appArtifact = builder.appArtifact; this.dependencies = builder.buildDependencies(); this.platformImports = builder.platformImports; - this.capabilityContracts = builder.extensionCapabilities; - this.localProjectArtifacts = builder.reloadableWorkspaceModules; + this.capabilityContracts = List.copyOf(builder.extensionCapabilities); + this.localProjectArtifacts = Set.copyOf(builder.reloadableWorkspaceModules); this.excludedResources = builder.excludedResources; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java index 641c677f562dd..cfc4b0539fbe9 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/DependencyFlags.java @@ -45,5 +45,4 @@ public interface DependencyFlags { */ int COMPILE_ONLY = 0b01000000000000; /* @formatter:on */ - } diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/CollectDependenciesBase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/CollectDependenciesBase.java index ade5086b19147..dc86cd8b16d82 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/CollectDependenciesBase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/CollectDependenciesBase.java @@ -49,8 +49,7 @@ public void testCollectedDependencies() throws Exception { } // stripping the resolved paths final List resolvedDeps = getTestResolver().resolveModel(root.toArtifact()).getDependencies() - .stream() - .map(d -> new ArtifactDependency(d)).collect(Collectors.toList()); + .stream().map(ArtifactDependency::new).collect(Collectors.toList()); assertEquals(new HashSet<>(expected), new HashSet<>(resolvedDeps)); } diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java index 880ef8079b0bb..5a5d2666f48e5 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; @@ -148,6 +149,7 @@ protected boolean isBootstrapForTestMode() { protected BootstrapAppModelResolver newAppModelResolver(LocalProject currentProject) throws Exception { final BootstrapAppModelResolver appModelResolver = new BootstrapAppModelResolver(newArtifactResolver(currentProject)); + appModelResolver.setIncubatingModelResolver(ApplicationDependencyModelResolver.isIncubatingEnabled(null)); if (isBootstrapForTestMode()) { appModelResolver.setTest(true); } diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java index 21004a9983db8..c13b3c844e6de 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/TsArtifact.java @@ -15,7 +15,6 @@ import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.GACT; -import io.quarkus.maven.dependency.GACTV; /** * @@ -180,6 +179,10 @@ public TsArtifact addDependency(TsDependency dep) { return this; } + public TsArtifact addManagedDependency(TsArtifact a) { + return addManagedDependency(new TsDependency(a)); + } + public TsArtifact addManagedDependency(TsDependency dep) { if (managedDeps.isEmpty()) { managedDeps = new ArrayList<>(); @@ -239,9 +242,10 @@ public Model getPomModel() { } if (!managedDeps.isEmpty()) { - model.setDependencyManagement(new DependencyManagement()); + var dm = new DependencyManagement(); + model.setDependencyManagement(dm); for (TsDependency dep : managedDeps) { - model.getDependencyManagement().addDependency(dep.toPomDependency()); + dm.addDependency(dep.toPomDependency()); } } @@ -252,7 +256,7 @@ public Model getPomModel() { } public ArtifactCoords toArtifact() { - return new GACTV(groupId, artifactId, classifier, type, version); + return ArtifactCoords.of(groupId, artifactId, classifier, type, version); } /** diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java index e7109757aa759..037ad003fea3f 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java @@ -34,8 +34,10 @@ import io.quarkus.bootstrap.BootstrapDependencyProcessingException; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.model.ApplicationModelBuilder; +import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyTreeResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; +import io.quarkus.bootstrap.resolver.maven.DependencyLoggingConfig; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.bootstrap.util.DependencyUtils; import io.quarkus.bootstrap.workspace.ArtifactSources; @@ -55,17 +57,34 @@ public class BootstrapAppModelResolver implements AppModelResolver { protected final MavenArtifactResolver mvn; - protected Consumer buildTreeConsumer; + private DependencyLoggingConfig depLogConfig; protected boolean devmode; protected boolean test; private boolean collectReloadableDeps = true; + private boolean incubatingModelResolver; public BootstrapAppModelResolver(MavenArtifactResolver mvn) { this.mvn = mvn; } + /** + * Temporary method that will be removed once the incubating implementation becomes the default. + * + * @return this application model resolver + */ + public BootstrapAppModelResolver setIncubatingModelResolver(boolean incubatingModelResolver) { + this.incubatingModelResolver = incubatingModelResolver; + return this; + } + public void setBuildTreeLogger(Consumer buildTreeConsumer) { - this.buildTreeConsumer = buildTreeConsumer; + if (buildTreeConsumer != null) { + depLogConfig = DependencyLoggingConfig.builder().setMessageConsumer(buildTreeConsumer).build(); + } + } + + public void setDepLogConfig(DependencyLoggingConfig depLogConfig) { + this.depLogConfig = depLogConfig; } /** @@ -328,13 +347,33 @@ private ApplicationModel buildAppModel(ResolvedDependency appArtifact, } var collectRtDepsRequest = MavenArtifactResolver.newCollectRequest(artifact, directDeps, managedDeps, List.of(), repos); try { - ApplicationDependencyTreeResolver.newInstance() - .setArtifactResolver(mvn) - .setApplicationModelBuilder(appBuilder) - .setCollectReloadableModules(collectReloadableDeps && reloadableModules.isEmpty()) - .setCollectCompileOnly(filteredProvidedDeps) - .setBuildTreeConsumer(buildTreeConsumer) - .resolve(collectRtDepsRequest); + long start = 0; + boolean logTime = false; + if (logTime) { + start = System.currentTimeMillis(); + } + if (incubatingModelResolver) { + ApplicationDependencyModelResolver.newInstance() + .setArtifactResolver(mvn) + .setApplicationModelBuilder(appBuilder) + .setCollectReloadableModules(collectReloadableDeps && reloadableModules.isEmpty()) + .setCollectCompileOnly(filteredProvidedDeps) + .setDependencyLogging(depLogConfig) + .resolve(collectRtDepsRequest); + } else { + ApplicationDependencyTreeResolver.newInstance() + .setArtifactResolver(mvn) + .setApplicationModelBuilder(appBuilder) + .setCollectReloadableModules(collectReloadableDeps && reloadableModules.isEmpty()) + .setCollectCompileOnly(filteredProvidedDeps) + .setBuildTreeConsumer(depLogConfig == null ? null : depLogConfig.getMessageConsumer()) + .resolve(collectRtDepsRequest); + } + if (logTime) { + System.err.println( + "Application model resolved in " + (System.currentTimeMillis() - start) + "ms, incubating=" + + incubatingModelResolver); + } } catch (BootstrapDependencyProcessingException e) { throw new AppModelResolverException( "Failed to inject extension deployment dependencies for " + appArtifact.toCompactCoords(), e); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyModelResolver.java new file mode 100644 index 0000000000000..9a3b9e1855b4e --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyModelResolver.java @@ -0,0 +1,1257 @@ +package io.quarkus.bootstrap.resolver.maven; + +import static io.quarkus.bootstrap.util.DependencyUtils.getCoords; +import static io.quarkus.bootstrap.util.DependencyUtils.getKey; +import static io.quarkus.bootstrap.util.DependencyUtils.getWinner; +import static io.quarkus.bootstrap.util.DependencyUtils.hasWinner; +import static io.quarkus.bootstrap.util.DependencyUtils.newDependencyBuilder; +import static io.quarkus.bootstrap.util.DependencyUtils.toArtifact; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.function.BiConsumer; + +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.collection.DependencyCollectionException; +import org.eclipse.aether.collection.DependencyGraphTransformationContext; +import org.eclipse.aether.collection.DependencySelector; +import org.eclipse.aether.graph.DefaultDependencyNode; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.graph.Exclusion; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.util.artifact.JavaScopes; +import org.eclipse.aether.util.graph.manager.DependencyManagerUtils; +import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector; +import org.eclipse.aether.util.graph.transformer.ConflictIdSorter; +import org.eclipse.aether.util.graph.transformer.ConflictResolver; +import org.jboss.logging.Logger; + +import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.BootstrapDependencyProcessingException; +import io.quarkus.bootstrap.model.ApplicationModelBuilder; +import io.quarkus.bootstrap.model.CapabilityContract; +import io.quarkus.bootstrap.model.PlatformImportsImpl; +import io.quarkus.bootstrap.resolver.AppModelResolverException; +import io.quarkus.bootstrap.util.BootstrapUtils; +import io.quarkus.bootstrap.util.DependencyUtils; +import io.quarkus.bootstrap.workspace.WorkspaceModule; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.DependencyFlags; +import io.quarkus.maven.dependency.ResolvedDependencyBuilder; +import io.quarkus.paths.PathTree; + +public class ApplicationDependencyModelResolver { + + private static final Logger log = Logger.getLogger(ApplicationDependencyModelResolver.class); + + private static final String QUARKUS_RUNTIME_ARTIFACT = "quarkus.runtime"; + private static final String QUARKUS_EXTENSION_DEPENDENCY = "quarkus.ext"; + + private static final String INCUBATING_MODEL_RESOLVER = "quarkus.bootstrap.incubating-model-resolver"; + + /* @formatter:off */ + private static final byte COLLECT_TOP_EXTENSION_RUNTIME_NODES = 0b001; + private static final byte COLLECT_DIRECT_DEPS = 0b010; + private static final byte COLLECT_RELOADABLE_MODULES = 0b100; + /* @formatter:on */ + + private static final Artifact[] NO_ARTIFACTS = new Artifact[0]; + + /** + * Temporary method that will be removed once this implementation becomes the default. + * + * @return true if this implementation is enabled + */ + public static boolean isIncubatingEnabled(Properties projectProperties) { + var value = System.getProperty(INCUBATING_MODEL_RESOLVER); + if (value == null && projectProperties != null) { + value = String.valueOf(projectProperties.get(INCUBATING_MODEL_RESOLVER)); + } + return Boolean.parseBoolean(value); + } + + public static ApplicationDependencyModelResolver newInstance() { + return new ApplicationDependencyModelResolver(); + } + + private final ExtensionInfo EXT_INFO_NONE = new ExtensionInfo(); + + private final List topExtensionDeps = new ArrayList<>(); + private final Map allExtensions = new ConcurrentHashMap<>(); + private List conditionalDepsToProcess = new ArrayList<>(); + + private final Map> artifactDeps = new HashMap<>(); + + private final Collection errors = new ConcurrentLinkedDeque<>(); + + private MavenArtifactResolver resolver; + private List managedDeps; + private ApplicationModelBuilder appBuilder; + private boolean collectReloadableModules; + private DependencyLoggingConfig depLogging; + private List collectCompileOnly; + + public ApplicationDependencyModelResolver setArtifactResolver(MavenArtifactResolver resolver) { + this.resolver = resolver; + return this; + } + + public ApplicationDependencyModelResolver setApplicationModelBuilder(ApplicationModelBuilder appBuilder) { + this.appBuilder = appBuilder; + return this; + } + + public ApplicationDependencyModelResolver setCollectReloadableModules(boolean collectReloadableModules) { + this.collectReloadableModules = collectReloadableModules; + return this; + } + + public ApplicationDependencyModelResolver setDependencyLogging(DependencyLoggingConfig depLogging) { + this.depLogging = depLogging; + return this; + } + + /** + * In addition to resolving dependencies for the build classpath, also resolve these compile-only dependencies + * and add them to the application model as {@link DependencyFlags#COMPILE_ONLY}. + * + * @param collectCompileOnly compile-only dependencies to add to the model + * @return self + */ + public ApplicationDependencyModelResolver setCollectCompileOnly(List collectCompileOnly) { + this.collectCompileOnly = collectCompileOnly; + return this; + } + + public void resolve(CollectRequest collectRtDepsRequest) throws AppModelResolverException { + this.managedDeps = collectRtDepsRequest.getManagedDependencies(); + // managed dependencies will be a bit augmented with every added extension, so let's load the properties early + collectPlatformProperties(); + this.managedDeps = managedDeps.isEmpty() ? new ArrayList<>() : managedDeps; + + DependencyNode root = resolveRuntimeDeps(collectRtDepsRequest); + processRuntimeDeps(root); + final List activatedConditionalDeps = activateConditionalDeps(); + + // resolve and inject deployment dependency branches for the top (first met) runtime extension nodes + injectDeployment(activatedConditionalDeps); + root = normalize(resolver.getSession(), root); + processDeploymentDeps(root); + + for (var d : appBuilder.getDependencies()) { + if (!d.isFlagSet(DependencyFlags.RELOADABLE) && !d.isFlagSet(DependencyFlags.VISITED)) { + clearReloadableFlag(d); + } + } + + for (var d : appBuilder.getDependencies()) { + d.clearFlag(DependencyFlags.VISITED); + if (d.isFlagSet(DependencyFlags.RELOADABLE)) { + appBuilder.addReloadableWorkspaceModule(d.getKey()); + } + appBuilder.addDependency(d); + } + + collectCompileOnly(collectRtDepsRequest, root); + } + + private List activateConditionalDeps() { + if (conditionalDepsToProcess.isEmpty()) { + return List.of(); + } + var activatedConditionalDeps = new ArrayList(); + boolean checkDependencyConditions = true; + while (!conditionalDepsToProcess.isEmpty() && checkDependencyConditions) { + checkDependencyConditions = false; + var unsatisfiedConditionalDeps = conditionalDepsToProcess; + conditionalDepsToProcess = new ArrayList<>(); + for (ConditionalDependency cd : unsatisfiedConditionalDeps) { + if (cd.isSatisfied()) { + cd.activate(); + activatedConditionalDeps.add(cd); + // if a dependency was activated, the remaining not satisfied conditions should be checked again + checkDependencyConditions = true; + } else { + conditionalDepsToProcess.add(cd); + } + } + } + return activatedConditionalDeps; + } + + private void processDeploymentDeps(DependencyNode root) { + var app = new AppDep(root); + var futures = new ArrayList>(); + app.scheduleChildVisits(futures, AppDep::scheduleDeploymentVisit); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + if (logErrors()) { + throw new RuntimeException( + "Failed to process Quarkus application deployment dependencies, please see the errors logged above for more details."); + } + for (var d : app.children) { + d.addToModel(); + } + + if (depLogging != null) { + new AppDepLogger().log(app); + } + } + + private boolean logErrors() { + if (!errors.isEmpty()) { + log.error("The following errors were encountered while processing Quarkus application dependencies:"); + var i = 1; + for (var error : errors) { + log.error(i++ + ")", error); + } + return true; + } + return false; + } + + private void injectDeployment(List activatedConditionalDeps) { + final List> futures = new ArrayList<>(topExtensionDeps.size() + + activatedConditionalDeps.size()); + for (ExtensionDependency extDep : topExtensionDeps) { + futures.add(CompletableFuture.supplyAsync(() -> { + var resolvedDep = appBuilder.getDependency(getKey(extDep.info.deploymentArtifact)); + if (resolvedDep == null) { + try { + extDep.collectDeploymentDeps(); + return () -> extDep.injectDeploymentNode(null); + } catch (BootstrapDependencyProcessingException e) { + errors.add(e); + } + } else { + // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath + // in which case we also clear the reloadable flag on it, in case it's coming from the workspace + resolvedDep.clearFlag(DependencyFlags.RELOADABLE); + } + return null; + })); + } + // non-conditional deployment branches should be added before the activated conditional ones to have consistent + // dependency graph structures + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + if (errors.isEmpty() && !activatedConditionalDeps.isEmpty()) { + for (ConditionalDependency cd : activatedConditionalDeps) { + futures.add(CompletableFuture.supplyAsync(() -> { + var resolvedDep = appBuilder.getDependency(getKey(cd.appDep.ext.info.deploymentArtifact)); + if (resolvedDep == null) { + var extDep = cd.getExtensionDependency(); + try { + extDep.collectDeploymentDeps(); + return () -> extDep.injectDeploymentNode(cd.appDep.ext.getParentDeploymentNode()); + } catch (BootstrapDependencyProcessingException e) { + errors.add(e); + } + } else { + // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath + // in which case we also clear the reloadable flag on it, in case it's coming from the workspace + resolvedDep.clearFlag(DependencyFlags.RELOADABLE); + } + return null; + })); + } + } + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + if (logErrors()) { + throw new RuntimeException( + "Failed to process Quarkus application deployment dependencies, please see the errors logged above for more details."); + } + + for (var future : futures) { + var ext = future.getNow(null); + if (ext != null) { + ext.run(); + } + } + } + + /** + * Resolves and adds compile-only dependencies to the application model with the {@link DependencyFlags#COMPILE_ONLY} flag. + * Compile-only dependencies are resolved as direct dependencies of the root with all the previously resolved dependencies + * enforced as version constraints to make sure compile-only dependencies do not override runtime dependencies of the final + * application. + * + * @param collectRtDepsRequest original runtime dependencies collection request + * @param root the root node of the Quarkus build time dependency tree + * @throws BootstrapMavenException in case of a failure + */ + private void collectCompileOnly(CollectRequest collectRtDepsRequest, DependencyNode root) throws BootstrapMavenException { + if (collectCompileOnly.isEmpty()) { + return; + } + // add all the build time dependencies as version constraints + var depStack = new ArrayDeque>(); + var children = root.getChildren(); + while (children != null) { + for (DependencyNode node : children) { + managedDeps.add(node.getDependency()); + if (!node.getChildren().isEmpty()) { + depStack.add(node.getChildren()); + } + } + children = depStack.poll(); + } + final CollectRequest request = new CollectRequest() + .setDependencies(collectCompileOnly) + .setManagedDependencies(managedDeps) + .setRepositories(collectRtDepsRequest.getRepositories()); + if (collectRtDepsRequest.getRoot() != null) { + request.setRoot(collectRtDepsRequest.getRoot()); + } else { + request.setRootArtifact(collectRtDepsRequest.getRootArtifact()); + } + + try { + root = resolver.getSystem().collectDependencies(resolver.getSession(), request).getRoot(); + } catch (DependencyCollectionException e) { + throw new BootstrapDependencyProcessingException( + "Failed to collect compile-only dependencies of " + root.getArtifact(), e); + } + children = root.getChildren(); + int flags = DependencyFlags.DIRECT | DependencyFlags.COMPILE_ONLY; + while (children != null) { + for (DependencyNode node : children) { + if (hasWinner(node)) { + continue; + } + var extInfo = getExtensionInfoOrNull(node.getArtifact(), node.getRepositories()); + var dep = appBuilder.getDependency(getKey(node.getArtifact())); + if (dep == null) { + dep = newDependencyBuilder(node, resolver).setFlags(flags); + if (extInfo != null) { + dep.setFlags(DependencyFlags.RUNTIME_EXTENSION_ARTIFACT); + if (dep.isFlagSet(DependencyFlags.DIRECT)) { + dep.setFlags(DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT); + } + } + appBuilder.addDependency(dep); + } else { + dep.setFlags(DependencyFlags.COMPILE_ONLY); + } + if (!node.getChildren().isEmpty()) { + depStack.add(node.getChildren()); + } + } + flags = DependencyFlags.COMPILE_ONLY; + children = depStack.poll(); + } + } + + private void collectPlatformProperties() throws AppModelResolverException { + final PlatformImportsImpl platformReleases = new PlatformImportsImpl(); + for (Dependency d : managedDeps) { + final Artifact artifact = d.getArtifact(); + final String extension = artifact.getExtension(); + final String artifactId = artifact.getArtifactId(); + if ("json".equals(extension) + && artifactId.endsWith(BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX)) { + platformReleases.addPlatformDescriptor(artifact.getGroupId(), artifactId, artifact.getClassifier(), extension, + artifact.getVersion()); + } else if ("properties".equals(extension) + && artifactId.endsWith(BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX)) { + platformReleases.addPlatformProperties(artifact.getGroupId(), artifactId, artifact.getClassifier(), extension, + artifact.getVersion(), resolver.resolve(artifact).getArtifact().getFile().toPath()); + } + } + appBuilder.setPlatformImports(platformReleases); + } + + private void clearReloadableFlag(ResolvedDependencyBuilder dep) { + final Set deps = artifactDeps.get(dep.getArtifactCoords()); + if (deps == null || deps.isEmpty()) { + return; + } + for (ArtifactKey key : deps) { + final ResolvedDependencyBuilder child = appBuilder.getDependency(key); + if (child == null || child.isFlagSet(DependencyFlags.VISITED)) { + continue; + } + child.setFlags(DependencyFlags.VISITED); + child.clearFlag(DependencyFlags.RELOADABLE); + clearReloadableFlag(child); + } + } + + private DependencyNode normalize(RepositorySystemSession session, DependencyNode root) throws AppModelResolverException { + final DependencyGraphTransformationContext context = new SimpleDependencyGraphTransformationContext(session); + try { + // resolves version conflicts + root = new ConflictIdSorter().transformGraph(root, context); + return session.getDependencyGraphTransformer().transformGraph(root, context); + } catch (RepositoryException e) { + throw new AppModelResolverException("Failed to resolve dependency graph conflicts", e); + } + } + + private DependencyNode resolveRuntimeDeps(CollectRequest request) + throws AppModelResolverException { + boolean verbose = true; //Boolean.getBoolean("quarkus.bootstrap.verbose-model-resolver"); + if (verbose) { + var session = resolver.getSession(); + final DefaultRepositorySystemSession mutableSession = new DefaultRepositorySystemSession(resolver.getSession()); + mutableSession.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, true); + mutableSession.setConfigProperty(DependencyManagerUtils.CONFIG_PROP_VERBOSE, true); + session = mutableSession; + + var ctx = new BootstrapMavenContext(BootstrapMavenContext.config() + .setRepositorySystem(resolver.getSystem()) + .setRepositorySystemSession(session) + .setRemoteRepositories(resolver.getRepositories()) + .setRemoteRepositoryManager(resolver.getRemoteRepositoryManager()) + .setCurrentProject(resolver.getMavenContext().getCurrentProject()) + .setWorkspaceDiscovery(collectReloadableModules)); + resolver = new MavenArtifactResolver(ctx); + } + try { + return resolver.getSystem().collectDependencies(resolver.getSession(), request).getRoot(); + } catch (DependencyCollectionException e) { + final Artifact a = request.getRoot() == null ? request.getRootArtifact() : request.getRoot().getArtifact(); + throw new BootstrapMavenException("Failed to resolve dependencies for " + a, e); + } + } + + private boolean isRuntimeArtifact(ArtifactKey key) { + final ResolvedDependencyBuilder dep = appBuilder.getDependency(key); + return dep != null && dep.isFlagSet(DependencyFlags.RUNTIME_CP); + } + + private void processRuntimeDeps(DependencyNode root) { + final AppDep app = new AppDep(root); + app.walkingFlags = COLLECT_TOP_EXTENSION_RUNTIME_NODES | COLLECT_DIRECT_DEPS; + if (collectReloadableModules) { + app.walkingFlags |= COLLECT_RELOADABLE_MODULES; + } + + var futures = new ArrayList>(); + app.scheduleChildVisits(futures, AppDep::scheduleRuntimeVisit); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + if (logErrors()) { + throw new RuntimeException( + "Failed to process Quarkus application runtime dependencies, please see the errors logged above for more details."); + } + app.setChildFlags(); + } + + private class AppDep { + final AppDep parent; + final DependencyNode node; + ExtensionDependency ext; + byte walkingFlags; + ResolvedDependencyBuilder resolvedDep; + final List children; + + AppDep(DependencyNode node) { + this.parent = null; + this.node = node; + this.children = new ArrayList<>(node.getChildren().size()); + } + + AppDep(AppDep parent, DependencyNode node) { + this.parent = parent; + this.node = node; + this.children = new ArrayList<>(node.getChildren().size()); + } + + void addToModel() { + for (var child : children) { + child.addToModel(); + } + // this node is added after its children to stay compatible with the legacy impl + if (resolvedDep != null) { + appBuilder.addDependency(resolvedDep); + } + } + + void scheduleDeploymentVisit(List> futures) { + futures.add(CompletableFuture.runAsync(() -> { + try { + visitDeploymentDependency(); + } catch (Throwable e) { + errors.add(e); + } + })); + scheduleChildVisits(futures, AppDep::scheduleDeploymentVisit); + } + + void visitDeploymentDependency() { + var dep = appBuilder.getDependency(getKey(node.getArtifact())); + if (dep == null) { + try { + resolvedDep = newDependencyBuilder(node, resolver).setFlags(DependencyFlags.DEPLOYMENT_CP); + } catch (BootstrapMavenException e) { + throw new RuntimeException(e); + } + } + } + + void scheduleRuntimeVisit(List> futures) { + futures.add(CompletableFuture.runAsync(() -> { + try { + visitRuntimeDependency(); + } catch (Throwable t) { + errors.add(t); + } + })); + scheduleChildVisits(futures, AppDep::scheduleRuntimeVisit); + } + + void visitRuntimeDependency() { + Artifact artifact = node.getArtifact(); + final ArtifactKey key = getKey(artifact); + if (resolvedDep == null) { + resolvedDep = appBuilder.getDependency(key); + } + + try { + var ext = getExtensionDependencyOrNull(); + if (resolvedDep == null) { + WorkspaceModule module = null; + if (resolver.getProjectModuleResolver() != null) { + module = resolver.getProjectModuleResolver().getProjectModule(artifact.getGroupId(), + artifact.getArtifactId(), artifact.getVersion()); + } + resolvedDep = DependencyUtils.toAppArtifact(getResolvedArtifact(), module) + .setOptional(node.getDependency().isOptional()) + .setScope(node.getDependency().getScope()) + .setRuntimeCp() + .setDeploymentCp(); + if (JavaScopes.PROVIDED.equals(resolvedDep.getScope())) { + resolvedDep.setFlags(DependencyFlags.COMPILE_ONLY); + } + if (ext != null) { + resolvedDep.setRuntimeExtensionArtifact(); + collectConditionalDependencies(); + } + } + } catch (DeploymentInjectionException e) { + throw e; + } catch (Exception t) { + throw new DeploymentInjectionException("Failed to inject extension deployment dependencies", t); + } + } + + void scheduleChildVisits(List> futures, + BiConsumer>> childVisitor) { + var childNodes = node.getChildren(); + List filtered = null; + var depKeys = artifactDeps.computeIfAbsent(getCoords(node.getArtifact()), key -> new HashSet<>(childNodes.size())); + for (int i = 0; i < childNodes.size(); ++i) { + var childNode = childNodes.get(i); + var winner = getWinner(childNode); + if (winner == null) { + depKeys.add(getKey(childNode.getArtifact())); + var child = new AppDep(this, childNode); + children.add(child); + if (filtered != null) { + filtered.add(childNode); + } + } else { + depKeys.add(getKey(winner.getArtifact())); + if (filtered == null) { + filtered = new ArrayList<>(childNodes.size()); + for (int j = 0; j < i; ++j) { + filtered.add(childNodes.get(j)); + } + } + } + } + if (filtered != null) { + node.setChildren(filtered); + } + for (var child : children) { + childVisitor.accept(child, futures); + } + } + + void setChildFlags() { + for (var c : children) { + c.setFlags(walkingFlags); + } + } + + void setFlags(byte walkingFlags) { + + if (ext != null) { + var parentExtDep = parent; + while (parentExtDep != null) { + if (parentExtDep.ext != null) { + parentExtDep.ext.addExtensionDependency(ext); + break; + } + parentExtDep = parentExtDep.parent; + } + ext.info.ensureActivated(); + } + + if (appBuilder.getDependency(resolvedDep.getKey()) == null) { + appBuilder.addDependency(resolvedDep); + if (ext != null) { + managedDeps.add(new Dependency(ext.info.deploymentArtifact, JavaScopes.COMPILE)); + } + } + this.walkingFlags = walkingFlags; + + resolvedDep.setDirect(isWalkingFlagOn(COLLECT_DIRECT_DEPS)); + if (ext != null && isWalkingFlagOn(COLLECT_TOP_EXTENSION_RUNTIME_NODES)) { + resolvedDep.setFlags(DependencyFlags.TOP_LEVEL_RUNTIME_EXTENSION_ARTIFACT); + clearWalkingFlag(COLLECT_TOP_EXTENSION_RUNTIME_NODES); + topExtensionDeps.add(ext); + } + if (isWalkingFlagOn(COLLECT_RELOADABLE_MODULES)) { + if (resolvedDep.getWorkspaceModule() != null + && !resolvedDep.isFlagSet(DependencyFlags.RUNTIME_EXTENSION_ARTIFACT)) { + resolvedDep.setReloadable(); + } else { + clearWalkingFlag(COLLECT_RELOADABLE_MODULES); + } + } + + clearWalkingFlag(COLLECT_DIRECT_DEPS); + + setChildFlags(); + } + + private ExtensionDependency getExtensionDependencyOrNull() + throws BootstrapDependencyProcessingException { + if (ext != null) { + return ext; + } + ext = ExtensionDependency.get(node); + if (ext == null) { + final ExtensionInfo extInfo = getExtensionInfoOrNull(node.getArtifact(), node.getRepositories()); + if (extInfo != null) { + ext = new ExtensionDependency(extInfo, node, collectExclusions()); + } + } + return ext; + } + + private Collection collectExclusions() { + if (parent == null) { + return List.of(); + } + Collection exclusions = null; + var next = this; + while (next != null) { + if (next.ext != null) { + if (exclusions == null) { + return next.ext.exclusions; + } + exclusions.addAll(next.ext.exclusions); + return exclusions; + } + var nextExcl = next.node.getDependency() == null ? null : next.node.getDependency().getExclusions(); + if (nextExcl != null && !nextExcl.isEmpty()) { + if (exclusions == null) { + exclusions = new ArrayList<>(nextExcl); + } + } + next = next.parent; + } + return exclusions == null ? List.of() : exclusions; + } + + Artifact getResolvedArtifact() { + var result = node.getArtifact(); + if (result.getFile() == null) { + result = resolve(result, node.getRepositories()); + node.setArtifact(result); + } + return result; + } + + private boolean isWalkingFlagOn(byte flag) { + return (walkingFlags & flag) > 0; + } + + private void clearWalkingFlag(byte flag) { + if ((walkingFlags & flag) > 0) { + walkingFlags ^= flag; + } + } + + private void collectConditionalDependencies() + throws BootstrapDependencyProcessingException { + if (ext.info.conditionalDeps.length == 0 || ext.conditionalDepsQueued) { + return; + } + ext.conditionalDepsQueued = true; + + final DependencySelector selector = ext.exclusions == null ? null + : new ExclusionDependencySelector(ext.exclusions); + for (Artifact conditionalArtifact : ext.info.conditionalDeps) { + if (selector != null && !selector.selectDependency(new Dependency(conditionalArtifact, JavaScopes.RUNTIME))) { + continue; + } + final ExtensionInfo conditionalInfo = getExtensionInfoOrNull(conditionalArtifact, + ext.runtimeNode.getRepositories()); + if (conditionalInfo == null) { + log.warn(ext.info.runtimeArtifact + " declares a conditional dependency on " + conditionalArtifact + + " that is not a Quarkus extension and will be ignored"); + continue; + } + if (conditionalInfo.activated) { + continue; + } + final ConditionalDependency conditionalDep = new ConditionalDependency(conditionalInfo, this); + conditionalDepsToProcess.add(conditionalDep); + conditionalDep.appDep.collectConditionalDependencies(); + } + } + } + + private ExtensionInfo getExtensionInfoOrNull(Artifact artifact, List repos) + throws BootstrapDependencyProcessingException { + if (!artifact.getExtension().equals(ArtifactCoords.TYPE_JAR)) { + return null; + } + final ArtifactKey extKey = getKey(artifact); + ExtensionInfo ext = allExtensions.get(extKey); + if (ext != null) { + return ext == EXT_INFO_NONE ? null : ext; + } + artifact = resolve(artifact, repos); + final Path path = artifact.getFile().toPath(); + final Properties descriptor = PathTree.ofDirectoryOrArchive(path).apply(BootstrapConstants.DESCRIPTOR_PATH, visit -> { + if (visit == null) { + return null; + } + try { + return readDescriptor(visit.getPath()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + if (descriptor == null) { + allExtensions.put(extKey, EXT_INFO_NONE); + return null; + } + ext = new ExtensionInfo(artifact, descriptor); + allExtensions.put(extKey, ext); + return ext; + } + + private DependencyNode collectDependencies(Artifact artifact, Collection exclusions, + List repos) { + DependencyNode root; + try { + root = resolver.getSystem() + .collectDependencies(resolver.getSession(), getCollectRequest(artifact, exclusions, repos)) + .getRoot(); + } catch (DependencyCollectionException e) { + throw new DeploymentInjectionException("Failed to collect dependencies for " + artifact, e); + } + if (root.getChildren().size() != 1) { + throw new DeploymentInjectionException("Only one child expected but got " + root.getChildren()); + } + return root.getChildren().get(0); + } + + private CollectRequest getCollectRequest(Artifact artifact, Collection exclusions, + List repos) { + final ArtifactDescriptorResult descr; + try { + descr = resolver.resolveDescriptor(artifact, repos); + } catch (BootstrapMavenException e) { + throw new DeploymentInjectionException("Failed to resolve descriptor for " + artifact, e); + } + final List allConstraints = new ArrayList<>( + managedDeps.size() + descr.getManagedDependencies().size()); + allConstraints.addAll(managedDeps); + allConstraints.addAll(descr.getManagedDependencies()); + return new CollectRequest() + .setManagedDependencies(allConstraints) + .setRepositories(repos) + .setRootArtifact(artifact) + .setDependencies(List.of(new Dependency(artifact, JavaScopes.COMPILE, false, exclusions))); + } + + private Artifact resolve(Artifact artifact, List repos) { + if (artifact.getFile() != null) { + return artifact; + } + try { + return resolver.getSystem().resolveArtifact(resolver.getSession(), + new ArtifactRequest() + .setArtifact(artifact) + .setRepositories(repos)) + .getArtifact(); + } catch (ArtifactResolutionException e) { + throw new DeploymentInjectionException("Failed to resolve artifact " + artifact, e); + } + } + + private static Properties readDescriptor(Path path) throws IOException { + final Properties rtProps = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(path)) { + rtProps.load(reader); + } + return rtProps; + } + + private class ExtensionInfo { + + final Artifact runtimeArtifact; + final Properties props; + final Artifact deploymentArtifact; + final Artifact[] conditionalDeps; + final ArtifactKey[] dependencyCondition; + boolean activated; + + private ExtensionInfo() { + runtimeArtifact = null; + props = null; + deploymentArtifact = null; + conditionalDeps = null; + dependencyCondition = null; + } + + ExtensionInfo(Artifact runtimeArtifact, Properties props) throws BootstrapDependencyProcessingException { + this.runtimeArtifact = runtimeArtifact; + this.props = props; + + String value = props.getProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT); + if (value == null) { + throw new BootstrapDependencyProcessingException("Extension descriptor from " + runtimeArtifact + + " does not include " + BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT); + } + Artifact deploymentArtifact = toArtifact(value); + if (deploymentArtifact.getVersion() == null || deploymentArtifact.getVersion().isEmpty()) { + deploymentArtifact = deploymentArtifact.setVersion(runtimeArtifact.getVersion()); + } + this.deploymentArtifact = deploymentArtifact; + + value = props.getProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES); + if (value != null) { + final String[] deps = BootstrapUtils.splitByWhitespace(value); + conditionalDeps = new Artifact[deps.length]; + for (int i = 0; i < deps.length; ++i) { + try { + conditionalDeps[i] = toArtifact(deps[i]); + } catch (Exception e) { + throw new BootstrapDependencyProcessingException( + "Failed to parse conditional dependencies configuration of " + runtimeArtifact, e); + } + } + } else { + conditionalDeps = NO_ARTIFACTS; + } + + dependencyCondition = BootstrapUtils + .parseDependencyCondition(props.getProperty(BootstrapConstants.DEPENDENCY_CONDITION)); + } + + void ensureActivated() { + if (activated) { + return; + } + activated = true; + appBuilder.handleExtensionProperties(props, runtimeArtifact.toString()); + + final String providesCapabilities = props.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); + final String requiresCapabilities = props.getProperty(BootstrapConstants.PROP_REQUIRES_CAPABILITIES); + if (providesCapabilities != null || requiresCapabilities != null) { + appBuilder.addExtensionCapabilities( + CapabilityContract.of(toCompactCoords(runtimeArtifact), providesCapabilities, requiresCapabilities)); + } + } + } + + private class ExtensionDependency { + + static ExtensionDependency get(DependencyNode node) { + return (ExtensionDependency) node.getData().get(QUARKUS_EXTENSION_DEPENDENCY); + } + + final ExtensionInfo info; + final DependencyNode runtimeNode; + final Collection exclusions; + boolean conditionalDepsQueued; + private List extDeps; + private DependencyNode deploymentNode; + private DependencyNode parentNode; + + ExtensionDependency(ExtensionInfo info, DependencyNode node, Collection exclusions) { + this.runtimeNode = node; + this.info = info; + this.exclusions = exclusions; + + @SuppressWarnings("unchecked") + final Map data = (Map) node.getData(); + if (data.isEmpty()) { + node.setData(QUARKUS_EXTENSION_DEPENDENCY, this); + } else if (data.put(QUARKUS_EXTENSION_DEPENDENCY, this) != null) { + throw new IllegalStateException( + "Dependency node " + node + " has already been associated with an extension dependency"); + } + } + + DependencyNode getParentDeploymentNode() { + if (parentNode == null) { + return null; + } + var ext = ExtensionDependency.get(parentNode); + if (ext == null) { + return null; + } + return ext.deploymentNode == null ? ext.parentNode : ext.deploymentNode; + } + + void addExtensionDependency(ExtensionDependency dep) { + if (extDeps == null) { + extDeps = new ArrayList<>(); + } + extDeps.add(dep); + } + + private void collectDeploymentDeps() + throws BootstrapDependencyProcessingException { + log.debugf("Collecting dependencies of %s", info.deploymentArtifact); + deploymentNode = collectDependencies(info.deploymentArtifact, exclusions, runtimeNode.getRepositories()); + if (deploymentNode.getChildren().isEmpty()) { + throw new BootstrapDependencyProcessingException( + "Failed to collect dependencies of " + deploymentNode.getArtifact() + + ": either its POM could not be resolved from the available Maven repositories " + + "or the artifact does not have any dependencies while at least a dependency on the runtime artifact " + + info.runtimeArtifact + " is expected"); + } + if (!replaceDirectDepBranch(deploymentNode, true)) { + throw new BootstrapDependencyProcessingException( + "Quarkus extension deployment artifact " + deploymentNode.getArtifact() + + " does not appear to depend on the corresponding runtime artifact " + + info.runtimeArtifact); + } + } + + private void injectDeploymentNode(DependencyNode parentDeploymentNode) { + if (parentDeploymentNode == null) { + runtimeNode.setData(QUARKUS_RUNTIME_ARTIFACT, runtimeNode.getArtifact()); + runtimeNode.setArtifact(deploymentNode.getArtifact()); + runtimeNode.setChildren(deploymentNode.getChildren()); + } else { + parentDeploymentNode.getChildren().add(deploymentNode); + } + } + + private boolean replaceDirectDepBranch(DependencyNode parentNode, boolean replaceRuntimeNode) { + int i = 0; + DependencyNode inserted = null; + var childNodes = parentNode.getChildren(); + while (i < childNodes.size()) { + var node = childNodes.get(i); + final Artifact a = node.getArtifact(); + if (a != null && !hasWinner(node) && isSameKey(info.runtimeArtifact, a)) { + // we are not comparing the version in the above condition because the runtime version + // may appear to be different from the deployment one and that's ok + // e.g. the version of the runtime artifact could be managed by a BOM + // but overridden by the user in the project config. The way the deployment deps + // are resolved here, the deployment version of the runtime artifact will be the one from the BOM. + if (replaceRuntimeNode) { + inserted = new DefaultDependencyNode(runtimeNode); + inserted.setChildren(runtimeNode.getChildren()); + childNodes.set(i, inserted); + } else { + inserted = runtimeNode; + } + if (this.deploymentNode == null && this.parentNode == null) { + this.parentNode = parentNode; + } + break; + } + ++i; + } + if (inserted == null) { + return false; + } + + if (extDeps != null) { + var depQueue = new ArrayList<>(childNodes); + var exts = new ArrayList<>(extDeps); + for (int j = 0; j < depQueue.size(); ++j) { + var depNode = depQueue.get(j); + if (hasWinner(depNode)) { + continue; + } + for (int k = 0; k < exts.size(); ++k) { + if (exts.get(k).replaceDirectDepBranch(depNode, replaceRuntimeNode && depNode != inserted)) { + exts.remove(k); + break; + } + } + if (exts.isEmpty()) { + break; + } + depQueue.addAll(depNode.getChildren()); + } + } + + return true; + } + } + + private class ConditionalDependency { + + final AppDep appDep; + private boolean activated; + + private ConditionalDependency(ExtensionInfo info, AppDep parent) { + final DefaultDependencyNode rtNode = new DefaultDependencyNode( + new Dependency(info.runtimeArtifact, JavaScopes.COMPILE)); + rtNode.setVersion(new BootstrapArtifactVersion(info.runtimeArtifact.getVersion())); + rtNode.setVersionConstraint(new BootstrapArtifactVersionConstraint( + new BootstrapArtifactVersion(info.runtimeArtifact.getVersion()))); + rtNode.setRepositories(parent.ext.runtimeNode.getRepositories()); + + appDep = new AppDep(parent, rtNode); + appDep.ext = new ExtensionDependency(info, rtNode, parent.ext.exclusions); + } + + ExtensionDependency getExtensionDependency() { + return appDep.ext; + } + + void activate() { + if (activated) { + return; + } + activated = true; + final ExtensionDependency extDep = getExtensionDependency(); + final DependencyNode originalNode = collectDependencies(appDep.ext.info.runtimeArtifact, extDep.exclusions, + extDep.runtimeNode.getRepositories()); + final DefaultDependencyNode rtNode = (DefaultDependencyNode) extDep.runtimeNode; + rtNode.setRepositories(originalNode.getRepositories()); + // if this node has conditional dependencies on its own, they may have been activated by this time + // in which case they would be included into its children + List currentChildren = rtNode.getChildren(); + if (currentChildren == null || currentChildren.isEmpty()) { + rtNode.setChildren(originalNode.getChildren()); + } else { + currentChildren.addAll(originalNode.getChildren()); + } + + appDep.walkingFlags = COLLECT_DIRECT_DEPS; + if (collectReloadableModules) { + appDep.walkingFlags |= COLLECT_RELOADABLE_MODULES; + } + var futures = new ArrayList>(); + appDep.scheduleRuntimeVisit(futures); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + if (logErrors()) { + throw new RuntimeException( + "Failed to process Quarkus application conditional dependencies, please see the errors logged above for more details."); + } + + appDep.setFlags(appDep.walkingFlags); + + var parentExtDep = appDep.parent; + parentExtDep.children.add(appDep); + while (parentExtDep != null) { + if (parentExtDep.ext != null) { + parentExtDep.ext.addExtensionDependency(appDep.ext); + break; + } + parentExtDep = parentExtDep.parent; + } + appDep.ext.info.ensureActivated(); + + appDep.parent.ext.runtimeNode.getChildren().add(rtNode); + } + + boolean isSatisfied() { + if (appDep.ext.info.dependencyCondition == null) { + return true; + } + for (ArtifactKey key : appDep.ext.info.dependencyCondition) { + if (!isRuntimeArtifact(key)) { + return false; + } + } + return true; + } + } + + private static boolean isSameKey(Artifact a1, Artifact a2) { + return a2.getArtifactId().equals(a1.getArtifactId()) + && a2.getGroupId().equals(a1.getGroupId()) + && a2.getClassifier().equals(a1.getClassifier()) + && a2.getExtension().equals(a1.getExtension()); + } + + private static String toCompactCoords(Artifact a) { + final StringBuilder b = new StringBuilder(); + b.append(a.getGroupId()).append(':').append(a.getArtifactId()).append(':'); + if (!a.getClassifier().isEmpty()) { + b.append(a.getClassifier()).append(':'); + } + if (!ArtifactCoords.TYPE_JAR.equals(a.getExtension())) { + b.append(a.getExtension()).append(':'); + } + b.append(a.getVersion()); + return b.toString(); + } + + private class AppDepLogger { + + final List depth = new ArrayList<>(); + + private AppDepLogger() { + } + + void log(AppDep root) { + logInternal(root); + + final int childrenTotal = root.children.size(); + if (childrenTotal > 0) { + if (childrenTotal == 1) { + depth.add(false); + log(root.children.get(0)); + } else { + depth.add(true); + int i = 0; + while (i < childrenTotal) { + log(root.children.get(i++)); + if (i == childrenTotal - 1) { + depth.set(depth.size() - 1, false); + } + } + } + depth.remove(depth.size() - 1); + } + } + + private void logInternal(AppDep dep) { + var buf = new StringBuilder(); + if (!depth.isEmpty()) { + for (int i = 0; i < depth.size() - 1; ++i) { + if (depth.get(i)) { + //buf.append("| "); + buf.append('\u2502').append(" "); + } else { + buf.append(" "); + } + } + if (depth.get(depth.size() - 1)) { + //buf.append("|- "); + buf.append('\u251c').append('\u2500').append(' '); + } else { + //buf.append("\\- "); + buf.append('\u2514').append('\u2500').append(' '); + } + } + buf.append(dep.node.getArtifact()); + if (!depth.isEmpty()) { + appendFlags(buf, getResolvedDependency(getKey(dep.node.getArtifact()))); + } + depLogging.getMessageConsumer().accept(buf.toString()); + + if (depLogging.isGraph()) { + var depKeys = artifactDeps.get(getCoords(dep.node.getArtifact())); + if (depKeys != null && !depKeys.isEmpty() && depKeys.size() != dep.children.size()) { + final Map versions = new HashMap<>(dep.children.size()); + for (var c : dep.children) { + versions.put(getKey(c.node.getArtifact()), c.node.getArtifact().getVersion()); + } + var list = new ArrayList(depKeys.size() - dep.children.size()); + for (var key : depKeys) { + if (!versions.containsKey(key)) { + var d = getResolvedDependency(key); + var sb = new StringBuilder().append(d.toGACTVString()); + appendFlags(sb, d); + list.add(sb.append(" [+]").toString()); + } + } + Collections.sort(list); + for (int j = 0; j < list.size(); ++j) { + buf = new StringBuilder(); + if (!depth.isEmpty()) { + for (int i = 0; i < depth.size() - 1; ++i) { + if (depth.get(i)) { + //buf.append("| "); + buf.append('\u2502').append(" "); + } else { + buf.append(" "); + } + } + if (depth.get(depth.size() - 1)) { + //buf.append("| "); + buf.append('\u2502').append(" "); + } else { + buf.append(" "); + } + } + + if (j < list.size() - 1) { + //buf.append("|- "); + buf.append('\u251c').append('\u2500').append(' '); + } else if (dep.children.isEmpty()) { + //buf.append("\\- "); + buf.append('\u2514').append('\u2500').append(' '); + } else { + //buf.append("|- "); + buf.append('\u251c').append('\u2500').append(' '); + } + buf.append(list.get(j)); + depLogging.getMessageConsumer().accept(buf.toString()); + } + } + } + } + + private void appendFlags(StringBuilder sb, ResolvedDependencyBuilder d) { + sb.append(" (").append(d.getScope()); + if (d.isFlagSet(DependencyFlags.OPTIONAL)) { + sb.append(" optional"); + } + if (depLogging.isVerbose()) { + if (d.isFlagSet(DependencyFlags.RUNTIME_CP)) { + sb.append(", runtime classpath"); + } else { + sb.append(", build-time classpath"); + } + if (d.isFlagSet(DependencyFlags.RUNTIME_EXTENSION_ARTIFACT)) { + sb.append(", extension"); + } + if (d.isFlagSet(DependencyFlags.RELOADABLE)) { + sb.append(", reloadable"); + } + } + sb.append(')'); + } + + private ResolvedDependencyBuilder getResolvedDependency(ArtifactKey key) { + var d = appBuilder.getDependency(key); + if (d == null) { + throw new IllegalArgumentException(key + " is not found among application dependencies"); + } + return d; + } + } +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java index 7a3fd7574fb5d..006cd4923d840 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java @@ -202,7 +202,6 @@ public void resolve(CollectRequest collectRtDepsRequest) throws AppModelResolver } } - // resolve and inject deployment dependency branches for the top (first met) runtime extension nodes for (ExtensionDependency extDep : topExtensionDeps) { injectDeploymentDependencies(extDep); } @@ -868,7 +867,7 @@ private ConditionalDependency(ExtensionInfo info, ExtensionDependency dependent) ExtensionDependency getExtensionDependency() { if (dependency == null) { final DefaultDependencyNode rtNode = new DefaultDependencyNode( - new Dependency(info.runtimeArtifact, JavaScopes.RUNTIME)); + new Dependency(info.runtimeArtifact, JavaScopes.COMPILE)); rtNode.setVersion(new BootstrapArtifactVersion(info.runtimeArtifact.getVersion())); rtNode.setVersionConstraint(new BootstrapArtifactVersionConstraint( new BootstrapArtifactVersion(info.runtimeArtifact.getVersion()))); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java index 14d4f5ef80f2b..c489b9c958b6a 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapModelResolver.java @@ -57,13 +57,7 @@ public List resolveArtifacts(RepositorySystemSession session, Collection requests) throws ArtifactResolutionException { return repoSystem.resolveArtifacts(session, requests); } - }, new VersionRangeResolver() { - @Override - public VersionRangeResult resolveVersionRange(RepositorySystemSession session, - VersionRangeRequest request) throws VersionRangeResolutionException { - return repoSystem.resolveVersionRange(session, request); - } - }, ctx.getRemoteRepositoryManager(), ctx.getRemoteRepositories()); + }, repoSystem::resolveVersionRange, ctx.getRemoteRepositoryManager(), ctx.getRemoteRepositories()); } private final RepositorySystemSession session; diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/DependencyLoggingConfig.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/DependencyLoggingConfig.java new file mode 100644 index 0000000000000..d9cb55946daac --- /dev/null +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/DependencyLoggingConfig.java @@ -0,0 +1,65 @@ +package io.quarkus.bootstrap.resolver.maven; + +import java.util.function.Consumer; + +public class DependencyLoggingConfig { + + public static Builder builder() { + return new DependencyLoggingConfig().new Builder(); + } + + public class Builder { + + private boolean built; + + private Builder() { + } + + public Builder setGraph(boolean graph) { + if (!built) { + DependencyLoggingConfig.this.graph = graph; + } + return this; + } + + public Builder setVerbose(boolean verbose) { + if (!built) { + DependencyLoggingConfig.this.verbose = verbose; + } + return this; + } + + public Builder setMessageConsumer(Consumer msgConsumer) { + if (!built) { + DependencyLoggingConfig.this.msgConsumer = msgConsumer; + } + return this; + } + + public DependencyLoggingConfig build() { + if (!built) { + built = true; + if (msgConsumer == null) { + throw new IllegalArgumentException("msgConsumer has not been initialized"); + } + } + return DependencyLoggingConfig.this; + } + } + + private boolean verbose; + private boolean graph; + private Consumer msgConsumer; + + public boolean isGraph() { + return graph; + } + + public boolean isVerbose() { + return verbose; + } + + public Consumer getMessageConsumer() { + return msgConsumer; + } +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/util/DependencyUtils.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/util/DependencyUtils.java index 66998179e9e7c..cc5f42ddec40a 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/util/DependencyUtils.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/util/DependencyUtils.java @@ -10,6 +10,7 @@ import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.util.graph.transformer.ConflictResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; @@ -152,4 +153,13 @@ public static ResolvedDependencyBuilder toAppArtifact(Artifact artifact, Workspa .setVersion(artifact.getVersion()) .setResolvedPaths(artifact.getFile() == null ? PathList.empty() : PathList.of(artifact.getFile().toPath())); } + + public static boolean hasWinner(DependencyNode node) { + return node.getData().containsKey(ConflictResolver.NODE_DATA_WINNER) && node.getChildren().isEmpty(); + } + + public static DependencyNode getWinner(DependencyNode node) { + final DependencyNode winner = (DependencyNode) node.getData().get(ConflictResolver.NODE_DATA_WINNER); + return winner == null || !node.getChildren().isEmpty() ? null : winner; + } } From e430046c26d0ad1a48146351456798932b4957f1 Mon Sep 17 00:00:00 2001 From: Jakub Jedlicka Date: Tue, 16 Apr 2024 14:54:16 +0200 Subject: [PATCH 58/72] Update datasource yaml config in docs --- docs/src/main/asciidoc/config-yaml.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/config-yaml.adoc b/docs/src/main/asciidoc/config-yaml.adoc index 074132c8bc895..8d420476d9517 100644 --- a/docs/src/main/asciidoc/config-yaml.adoc +++ b/docs/src/main/asciidoc/config-yaml.adoc @@ -79,7 +79,8 @@ quarkus: ---- quarkus: datasource: - url: jdbc:postgresql://localhost:5432/quarkus_test + jdbc: + url: jdbc:postgresql://localhost:5432/quarkus_test hibernate-orm: database: From 29a05883d2277274db2b9ac0ab886a6c0c1d0ad6 Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Tue, 16 Apr 2024 16:32:42 +0200 Subject: [PATCH 59/72] Bump smallrye-reactive-messaging.version from 4.20.0 to 4.21.0 --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 382ec161b8619..1bd26051ec769 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -62,7 +62,7 @@ 1.0.13 3.0.1 3.12.0 - 4.20.0 + 4.21.0 2.6.0 2.1.3 2.1.1 From 6afb99031d3adae9e92cb76fcd6f3feebcd062ac Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Mon, 15 Apr 2024 09:46:54 +0200 Subject: [PATCH 60/72] Introduce workflow for testing GraalVM variants This commit introduces a new workflow tailored for testing GraalVM variants. Derived from the existing incremental workflow, it focuses on executing native integration tests with selected GraalVM variants such as CE, EE, Mandrel, Liberica, and versions 17, 21, and 22. This workflow is designed specifically for testing purposes and should be run on-demand rather than being triggered with every PR. It's recommended to execute this workflow from a fork to conserve QuarkusIO CI resources. --- .../workflows/native-it-selected-graalvm.yml | 396 ++++++++++++++++++ 1 file changed, 396 insertions(+) create mode 100644 .github/workflows/native-it-selected-graalvm.yml diff --git a/.github/workflows/native-it-selected-graalvm.yml b/.github/workflows/native-it-selected-graalvm.yml new file mode 100644 index 0000000000000..8103432d9f1df --- /dev/null +++ b/.github/workflows/native-it-selected-graalvm.yml @@ -0,0 +1,396 @@ +name: Quarkus CI - Native IT on selected GraalVM + +on: + workflow_dispatch: + inputs: + BRANCH: + description: 'Branch to use' + required: true + default: 'main' + type: string + NATIVE_COMPILER: + description: 'the native compiler to use' + required: true + default: 'mandrel' + type: choice + options: + - mandrel + - graalvm-community + - graalvm + - liberica + NATIVE_COMPILER_VERSION: + description: 'the native compiler version to use' + required: true + default: '21' + type: choice + options: + - '17' + - '21' + - '22' + +env: + # Workaround testsuite locale issue + LANG: en_US.UTF-8 + COMMON_MAVEN_ARGS: "-e -B --settings .github/mvn-settings.xml --fail-at-end" + COMMON_TEST_MAVEN_ARGS: "-Dformat.skip -Denforcer.skip -DskipDocs -Dforbiddenapis.skip -DskipExtensionValidation -DskipCodestartValidation" + NATIVE_TEST_MAVEN_ARGS: "-Dtest-containers -Dstart-containers -Dquarkus.native.native-image-xmx=6g -Dnative -Dnative.surefire.skip -Dno-descriptor-tests clean install -DskipDocs" + JVM_TEST_MAVEN_ARGS: "-Dtest-containers -Dstart-containers -Dquarkus.test.hang-detection-timeout=60" + PTS_MAVEN_ARGS: "-Ddevelocity.pts.enabled=${{ github.event_name == 'pull_request' && github.base_ref == 'main' && 'true' || 'false' }}" + DB_USER: hibernate_orm_test + DB_PASSWORD: hibernate_orm_test + DB_NAME: hibernate_orm_test + DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} + PULL_REQUEST_NUMBER: ${{ github.event.number }} + +defaults: + run: + shell: bash + +jobs: + build-jdk17: + name: "Initial JDK 17 Build - ${{ inputs.BRANCH }}" + runs-on: ubuntu-latest + outputs: + gib_args: ${{ steps.get-gib-args.outputs.gib_args }} + gib_impacted: ${{ steps.get-gib-impacted.outputs.impacted_modules }} + m2-cache-key: ${{ steps.m2-cache-key.outputs.key }} + steps: + - name: Gradle Enterprise environment + run: | + echo "GE_TAGS=jdk-17" >> "$GITHUB_ENV" + echo "GE_CUSTOM_VALUES=gh-job-name=Initial JDK 17 Build" >> "$GITHUB_ENV" + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.BRANCH }} + # this is important for GIB to work + fetch-depth: 0 + - name: Add quarkusio remote + run: git remote show quarkusio &> /dev/null || git remote add quarkusio https://github.com/quarkusio/quarkus.git + - name: Reclaim Disk Space + run: .github/ci-prerequisites.sh + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Generate .m2 cache key + id: m2-cache-key + run: | + echo "key=m2-cache-$(/bin/date -u "+%Y-%U")" >> $GITHUB_OUTPUT + - name: Cache Maven Repository + id: cache-maven + uses: actions/cache@v4 + with: + path: ~/.m2/repository + # refresh cache every week to avoid unlimited growth + key: ${{ steps.m2-cache-key.outputs.key }} + - name: Verify native-tests.json + run: ./.github/verify-tests-json.sh native-tests.json integration-tests/ + - name: Verify virtual-threads-tests.json + run: ./.github/verify-tests-json.sh virtual-threads-tests.json integration-tests/virtual-threads/ + - name: Setup Develocity Build Scan capture + uses: gradle/develocity-actions/maven-setup@v1 + with: + capture-strategy: ON_DEMAND + job-name: "Initial JDK 17 Build" + add-pr-comment: false + add-job-summary: false + - name: Build + env: + CAPTURE_BUILD_SCAN: true + run: | + ./mvnw -T1C $COMMON_MAVEN_ARGS -DskipTests -DskipITs -DskipDocs -Dinvoker.skip -Dskip.gradle.tests -Djbang.skip -Dtruststore.skip -Dno-format -Dtcks -Prelocations clean install + - name: Verify extension dependencies + run: ./update-extension-dependencies.sh $COMMON_MAVEN_ARGS + - name: Get GIB arguments + id: get-gib-args + env: + PULL_REQUEST_BASE: ${{ github.event.pull_request.base.ref }} + run: | + # See also: https://github.com/gitflow-incremental-builder/gitflow-incremental-builder#configuration (GIB) + # Common GIB_ARGS for all CI cases (hint: see also root pom.xml): + # - disableSelectedProjectsHandling: required to detect changes in jobs that use -pl + # - untracked: to ignore files created by jobs (and uncommitted to be consistent) + GIB_ARGS="-Dincremental -Dgib.disableSelectedProjectsHandling -Dgib.untracked=false -Dgib.uncommitted=false" + if [ -n "$PULL_REQUEST_BASE" ] + then + # The PR defines a clear merge target so just use that branch for reference, *unless*: + # - the current branch is a backport branch targeting some released branch like 1.10 (merge target is not main) + GIB_ARGS+=" -Dgib.referenceBranch=origin/$PULL_REQUEST_BASE -Dgib.disableIfReferenceBranchMatches='origin/\d+\.\d+'" + else + # No PR means the merge target is uncertain so fetch & use main of quarkusio/quarkus, *unless*: + # - the current branch is main or some released branch like 1.10 + # - the current branch is a backport branch which is going to target some released branch like 1.10 (merge target is not main) + GIB_ARGS+=" -Dgib.referenceBranch=refs/remotes/quarkusio/main -Dgib.fetchReferenceBranch -Dgib.disableIfBranchMatches='main|\d+\.\d+|.*backport.*'" + fi + echo "GIB_ARGS: $GIB_ARGS" + echo "gib_args=${GIB_ARGS}" >> $GITHUB_OUTPUT + - name: Get GIB impacted modules + id: get-gib-impacted + # mvnw just for creating gib-impacted.log ("validate" should not waste much time if not incremental at all, e.g. on main) + run: | + ./mvnw -q -T1C $COMMON_MAVEN_ARGS -Dscan=false -Dtcks -Dquickly-ci ${{ steps.get-gib-args.outputs.gib_args }} -Dgib.logImpactedTo=gib-impacted.log validate + if [ -f gib-impacted.log ] + then + GIB_IMPACTED=$(cat gib-impacted.log) + else + GIB_IMPACTED='_all_' + fi + echo "GIB_IMPACTED: ${GIB_IMPACTED}" + # three steps to retain linefeeds in output for other jobs + # (see https://github.com/github/docs/issues/21529 and https://github.com/orgs/community/discussions/26288#discussioncomment-3876281) + echo 'impacted_modules<> $GITHUB_OUTPUT + echo "${GIB_IMPACTED}" >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + - name: Tar .m2/repository/io/quarkus + run: tar -czf m2-io-quarkus.tgz -C ~ .m2/repository/io/quarkus + - name: Upload .m2/repository/io/quarkus + uses: actions/upload-artifact@v4 + with: + name: m2-io-quarkus + path: m2-io-quarkus.tgz + retention-days: 7 + - name: Delete snapshots artifacts from cache + run: find ~/.m2 -name \*-SNAPSHOT -type d -exec rm -rf {} + + - name: Prepare build reports archive + if: always() + run: | + 7z a -tzip build-reports.zip -r \ + 'target/build-report.json' \ + 'target/gradle-build-scan-url.txt' \ + LICENSE + - name: Upload build reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: "build-reports-Initial JDK 17 Build" + path: | + build-reports.zip + retention-days: 7 + + calculate-test-jobs: + name: Calculate Test Jobs + runs-on: ubuntu-latest + needs: build-jdk17 + env: + GIB_IMPACTED_MODULES: ${{ needs.build-jdk17.outputs.gib_impacted }} + outputs: + native_matrix: ${{ steps.calc-native-matrix.outputs.matrix }} + virtual_threads_matrix: ${{ steps.calc-virtual-threads-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + - name: Calculate matrix from native-tests.json + id: calc-native-matrix + run: | + echo "GIB_IMPACTED_MODULES: ${GIB_IMPACTED_MODULES}" + json=$(.github/filter-native-tests-json.sh "${GIB_IMPACTED_MODULES}" | tr -d '\n') + # Remove Windows from the matrix + json=$(echo $json | jq 'del(.include[] | select(."os-name" == "windows-latest"))') + json=$(echo $json | tr -d '\n') + echo "${json}" + echo "matrix=${json}" >> $GITHUB_OUTPUT + - name: Calculate matrix from virtual-threads-tests.json + id: calc-virtual-threads-matrix + run: | + echo "GIB_IMPACTED_MODULES: ${GIB_IMPACTED_MODULES}" + json=$(.github/filter-virtual-threads-tests-json.sh "${GIB_IMPACTED_MODULES}" | tr -d '\n') + # Remove Windows from the matrix + json=$(echo $json | jq 'del(.include[] | select(."os-name" == "windows-latest"))') + json=$(echo $json | tr -d '\n') + echo "${json}" + echo "matrix=${json}" >> $GITHUB_OUTPUT + + virtual-thread-native-tests: + name: Native Tests - Virtual Thread - ${{matrix.category}} - ${{inputs.NATIVE_COMPILER}} ${{inputs.NATIVE_COMPILER_VERSION}} - ${{inputs.BRANCH}} + runs-on: ${{matrix.os-name}} + needs: [build-jdk17, calculate-test-jobs] + timeout-minutes: ${{matrix.timeout}} + strategy: + max-parallel: 12 + fail-fast: false + matrix: ${{ fromJson(needs.calculate-test-jobs.outputs.virtual_threads_matrix) }} + steps: + - name: Gradle Enterprise environment + run: | + category=$(echo -n '${{matrix.category}}' | tr '[:upper:]' '[:lower:]' | tr -c '[:alnum:]-' '-' | sed -E 's/-+/-/g') + echo "GE_TAGS=virtual-thread-native-${category}" >> "$GITHUB_ENV" + echo "GE_CUSTOM_VALUES=gh-job-name=Native Tests - Virtual Thread - ${{matrix.category}}" >> "$GITHUB_ENV" + - uses: actions/checkout@v4 + - name: Restore Maven Repository + uses: actions/cache/restore@v4 + with: + path: ~/.m2/repository + # refresh cache every week to avoid unlimited growth + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} + - name: Download .m2/repository/io/quarkus + uses: actions/download-artifact@v4 + with: + name: m2-io-quarkus + path: . + - name: Extract .m2/repository/io/quarkus + run: tar -xzf m2-io-quarkus.tgz -C ~ + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: Setup GraalVM + id: setup-graalvm + uses: graalvm/setup-graalvm@v1 + with: + java-version: ${{ inputs.NATIVE_COMPILER_VERSION }} + distribution: ${{ inputs.NATIVE_COMPILER }} + github-token: ${{ secrets.GITHUB_TOKEN }} + # We do this so we can get better analytics for the downloaded version of the build images + - name: Update Docker Client User Agent + run: | + if [ -f ~/.docker/config.json ]; then + cat <<< $(jq '.HttpHeaders += {"User-Agent": "Quarkus-CI-Docker-Client"}' ~/.docker/config.json) > ~/.docker/config.json + fi + - name: Setup Develocity Build Scan capture + uses: gradle/develocity-actions/maven-setup@v1 + with: + capture-strategy: ON_DEMAND + job-name: "Native Tests - Virtual Thread - ${{matrix.category}}" + add-pr-comment: false + add-job-summary: false + - name: Build + env: + TEST_MODULES: ${{matrix.test-modules}} + CAPTURE_BUILD_SCAN: true + run: | + export LANG=en_US && ./mvnw $COMMON_MAVEN_ARGS $COMMON_TEST_MAVEN_ARGS $PTS_MAVEN_ARGS -f integration-tests/virtual-threads -pl "$TEST_MODULES" $NATIVE_TEST_MAVEN_ARGS + - name: Prepare build reports archive + if: always() + run: | + 7z a -tzip build-reports.zip -r \ + 'integration-tests/virtual-threads/**/target/*-reports/TEST-*.xml' \ + 'integration-tests/virtual-threads/target/build-report.json' \ + 'integration-tests/virtual-threads/target/gradle-build-scan-url.txt' \ + LICENSE + - name: Upload build reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: "build-reports-Virtual Thread Support Tests Native - ${{matrix.category}}" + path: | + build-reports.zip + retention-days: 7 + + native-tests: + name: Native Tests - ${{matrix.category}} - ${{inputs.NATIVE_COMPILER}} ${{inputs.NATIVE_COMPILER_VERSION}} - ${{inputs.BRANCH}} + needs: [build-jdk17, calculate-test-jobs] + runs-on: ${{matrix.os-name}} + env: + # leave more space for the actual native compilation and execution + MAVEN_OPTS: -Xmx1g + # Ignore the following YAML Schema error + timeout-minutes: ${{matrix.timeout}} + strategy: + max-parallel: 12 + fail-fast: false + matrix: ${{ fromJson(needs.calculate-test-jobs.outputs.native_matrix) }} + steps: + - name: Gradle Enterprise environment + run: | + category=$(echo -n '${{matrix.category}}' | tr '[:upper:]' '[:lower:]' | tr -c '[:alnum:]-' '-' | sed -E 's/-+/-/g') + echo "GE_TAGS=native-${category}" >> "$GITHUB_ENV" + echo "GE_CUSTOM_VALUES=gh-job-name=Native Tests - ${{matrix.category}}" >> "$GITHUB_ENV" + - uses: actions/checkout@v4 + - name: Reclaim Disk Space + run: .github/ci-prerequisites.sh + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Setup GraalVM + id: setup-graalvm + uses: graalvm/setup-graalvm@v1 + with: + java-version: ${{ inputs.NATIVE_COMPILER_VERSION }} + distribution: ${{ inputs.NATIVE_COMPILER }} + github-token: ${{ secrets.GITHUB_TOKEN }} + # We do this so we can get better analytics for the downloaded version of the build images + - name: Update Docker Client User Agent + run: | + if [ -f ~/.docker/config.json ]; then + cat <<< $(jq '.HttpHeaders += {"User-Agent": "Quarkus-CI-Docker-Client"}' ~/.docker/config.json) > ~/.docker/config.json + fi + - name: Restore Maven Repository + uses: actions/cache/restore@v4 + with: + path: ~/.m2/repository + # refresh cache every week to avoid unlimited growth + key: ${{ needs.build-jdk17.outputs.m2-cache-key }} + - name: Download .m2/repository/io/quarkus + uses: actions/download-artifact@v4 + with: + name: m2-io-quarkus + path: . + - name: Extract .m2/repository/io/quarkus + run: tar -xzf m2-io-quarkus.tgz -C ~ + - name: Setup Develocity Build Scan capture + uses: gradle/develocity-actions/maven-setup@v1 + with: + capture-strategy: ON_DEMAND + job-name: "Native Tests - ${{matrix.category}}" + add-pr-comment: false + add-job-summary: false + - name: Cache Quarkus metadata + uses: actions/cache@v4 + with: + path: '**/.quarkus/quarkus-prod-config-dump' + key: ${{ runner.os }}-quarkus-metadata + - name: Build + env: + TEST_MODULES: ${{matrix.test-modules}} + CAPTURE_BUILD_SCAN: true + run: ./mvnw $COMMON_MAVEN_ARGS $COMMON_TEST_MAVEN_ARGS $PTS_MAVEN_ARGS -f integration-tests -pl "$TEST_MODULES" $NATIVE_TEST_MAVEN_ARGS + - name: Prepare failure archive (if maven failed) + if: failure() + run: find . -type d -name '*-reports' -o -wholename '*/build/reports/tests/functionalTest' -o -name '*.log' | tar -czf test-reports.tgz -T - + - name: Upload failure Archive (if maven failed) + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-reports-native-${{matrix.category}} + path: 'test-reports.tgz' + retention-days: 7 + - name: Prepare build reports archive + if: always() + run: | + 7z a -tzip build-reports.zip -r \ + '**/target/*-reports/TEST-*.xml' \ + '**/build/test-results/test/TEST-*.xml' \ + 'target/build-report.json' \ + 'target/gradle-build-scan-url.txt' \ + LICENSE + - name: Upload build reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: "build-reports-Native Tests - ${{matrix.category}}" + path: | + build-reports.zip + retention-days: 7 + + build-report: + runs-on: ubuntu-latest + name: Build report - ${{inputs.NATIVE_COMPILER}} ${{inputs.NATIVE_COMPILER_VERSION}} - ${{inputs.BRANCH}} + needs: [build-jdk17,native-tests,virtual-thread-native-tests] + if: always() + steps: + - uses: actions/download-artifact@v4 + with: + path: build-reports-artifacts + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Produce report and add it as job summary + uses: quarkusio/action-build-reporter@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + build-reports-artifacts-path: build-reports-artifacts From b9be05fc2138be29bbd8b5b7c0a9b89cdb6fb00c Mon Sep 17 00:00:00 2001 From: Ivan Puntev Date: Mon, 15 Apr 2024 19:26:10 +0300 Subject: [PATCH 61/72] Adding possibility to supply the jwt private key as a string. --- .../security-oidc-code-flow-authentication.adoc | 9 +++++++++ ...security-openid-connect-client-reference.adoc | 9 +++++++++ ...dcClientCredentialsJwtPrivateKeyTestCase.java | 10 ++++++++++ .../quarkus/oidc/client/OidcClientResource.java | 14 ++++++++++++-- .../quarkus/oidc/client/OidcClientsResource.java | 2 +- ...client-credentials-jwt-private-key.properties | 4 ++++ .../quarkus/oidc/common/OidcRequestFilter.java | 2 +- .../oidc/common/runtime/OidcCommonConfig.java | 16 ++++++++++++++++ .../oidc/common/runtime/OidcCommonUtils.java | 9 ++++++--- 9 files changed, 68 insertions(+), 7 deletions(-) diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc index 6eb16fe378aef..06868101eb6d5 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -169,6 +169,15 @@ quarkus.oidc.credentials.jwt.secret-provider.key=mysecret-key quarkus.oidc.credentials.jwt.secret-provider.name=oidc-credentials-provider ---- +Example of `private_key_jwt` with the PEM key inlined in application.properties, and where the signature algorithm is `RS256`: + +[source,properties] +---- +quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/ +quarkus.oidc.client-id=quarkus-app +quarkus.oidc.credentials.jwt.key=Base64-encoded private key representation +---- + Example of `private_key_jwt` with the PEM key file, and where the signature algorithm is RS256: [source,properties] diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index 48a482d4b2091..db214a15dbe15 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -757,6 +757,15 @@ quarkus.oidc-client.credentials.jwt.secret-provider.key=mysecret-key quarkus.oidc-client.credentials.jwt.secret-provider.name=oidc-credentials-provider ---- +`private_key_jwt` with the PEM key inlined in application.properties, and where the signature algorithm is `RS256`: + +[source,properties] +---- +quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/ +quarkus.oidc-client.client-id=quarkus-app +quarkus.oidc-client.credentials.jwt.key=Base64-encoded private key representation +---- + `private_key_jwt` with the PEM key file, signature algorithm is `RS256`: [source,properties] diff --git a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientCredentialsJwtPrivateKeyTestCase.java b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientCredentialsJwtPrivateKeyTestCase.java index 46e96feb39b37..d8252bd1a9614 100644 --- a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientCredentialsJwtPrivateKeyTestCase.java +++ b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientCredentialsJwtPrivateKeyTestCase.java @@ -34,4 +34,14 @@ public void testClientCredentialsToken() { .statusCode(200) .body(equalTo("service-account-quarkus-app")); } + + @Test + public void testPrivateKeyToken() { + String token = RestAssured.when().get("/client/token-key").body().asString(); + RestAssured.given().auth().oauth2(token) + .when().get("/protected") + .then() + .statusCode(200) + .body(equalTo("service-account-quarkus-app")); + } } diff --git a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientResource.java b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientResource.java index 75c38a064d0ec..bad7734d50387 100644 --- a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientResource.java +++ b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientResource.java @@ -14,6 +14,16 @@ public class OidcClientResource { @Inject OidcClient client; + @Inject + @NamedOidcClient("key") + OidcClient keyClient; + + @GET + @Path("token-key") + public Uni tokenFromPrivateKeyUni() { + return keyClient.getTokens().flatMap(tokens -> Uni.createFrom().item(tokens.getAccessToken())); + } + @GET @Path("token") public Uni tokenUni() { @@ -23,13 +33,13 @@ public Uni tokenUni() { @GET @Path("tokens") public Uni grantTokensUni() { - return client.getTokens().flatMap(tokens -> createTokensString(tokens)); + return client.getTokens().flatMap(this::createTokensString); } @GET @Path("refresh-tokens") public Uni refreshGrantTokens(@QueryParam("refreshToken") String refreshToken) { - return client.refreshTokens(refreshToken).flatMap(tokens -> createTokensString(tokens)); + return client.refreshTokens(refreshToken).flatMap(this::createTokensString); } private Uni createTokensString(Tokens tokens) { diff --git a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientsResource.java b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientsResource.java index e575a1e45fa92..2cb8795032d7a 100644 --- a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientsResource.java +++ b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientsResource.java @@ -27,7 +27,7 @@ public Uni tokenUni(@PathParam("id") String oidcClientId) { @GET @Path("tokens/{id}") public Uni grantTokensUni(@PathParam("id") String oidcClientId) { - return getClient(oidcClientId).getTokens().flatMap(tokens -> createTokensString(tokens)); + return getClient(oidcClientId).getTokens().flatMap(this::createTokensString); } @GET diff --git a/extensions/oidc-client/deployment/src/test/resources/application-oidc-client-credentials-jwt-private-key.properties b/extensions/oidc-client/deployment/src/test/resources/application-oidc-client-credentials-jwt-private-key.properties index 593d2f8161cdf..93ce6084ed4f8 100644 --- a/extensions/oidc-client/deployment/src/test/resources/application-oidc-client-credentials-jwt-private-key.properties +++ b/extensions/oidc-client/deployment/src/test/resources/application-oidc-client-credentials-jwt-private-key.properties @@ -4,3 +4,7 @@ quarkus.oidc.client-id=quarkus-app quarkus.oidc-client.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc-client.client-id=${quarkus.oidc.client-id} quarkus.oidc-client.credentials.jwt.key-file=/privateKey.pem + +quarkus.oidc-client.key.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.key.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.key.credentials.jwt.key=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCyXwKqKL/hQWDkurdHyRn/9aZqmrgpCfiT5+gQ7KZ9RvDjgTqkJT6IIrRFvIpeBMwSsw3dkUPGmgN1J4QOhLaR2VEXhc20UbxFbr6HXAskZGPuCL1tzRWDkLNMZaEO8jqhPbcq1Ro4GMhaSdm0sBHmcQnu8wAOrdAowdzGh/HUaaYBDY0OZVAm9N8zzBXTahna9frJCMHq3e9szIiv6HYZTy1672/+hR/0D1HY+bqpQtJnzSrKjkFeXDAbYPgewYLEJ2Dk+oo6L1I6S+UTrl4FRHw1fHAd2i75JD+vL/8w/AtKkej0CCBUSZJiV+KDJWjnDUVRWjq5hQb9pu4qEJKhAgMBAAECggEAJvBs4X7B3MfsAiLszgQN4/3ZlZ4vI+5kUM2osMEo22J4RgI5Lgpfa1LALhUp07qSXmauWTdUJ3AJ3zKANrcsMAzUEiGItZu+UR4LA/vJBunPkvBfgi/qSW12ZvAsx9mDiR2y9evNrH9khalnmHVzgu4ccAimc43oSm1/5+tXlLoZ1QK/FohxBxAshtuDHGs8yKUL0jpv7dOrjhCj2ibmPYe6AUk9F61sVWO0/i0Q8UAOcYT3L5nCS5WnLhdCdYpIJJ7xl2PrVE/BAD+JEG5uCOYfVeYh+iCZVfpX17ryfNNUaBtyxKEGVtHbje3mO86mYN3noaS0w/zpUjBPgV+KEQKBgQDsp6VTmDIqHFTp2cC2yrDMxRznif92EGv7ccJDZtbTC37mAuf2J7x5b6AiE1EfxEXyGYzSk99sCns+GbL1EHABUt5pimDCl33b6XvuccQNpnJ0MfM5eRX9Ogyt/OKdDRnQsvrTPNCWOyJjvG01HQM4mfxaBBnxnvl5meH2pyG/ZQKBgQDA87DnyqEFhTDLX5c1TtwHSRj2xeTPGKG0GyxOJXcxR8nhtY9ee0kyLZ14RytnOxKarCFgYXeG4IoGEc/I42WbA4sq88tZcbe4IJkdX0WLMqOTdMrdx9hMU1ytKVUglUJZBVm7FaTQjA+ArMwqkXAA5HBMtArUsfJKUt3l0hMIjQKBgQDS1vmAZJQs2Fj+jzYWpLaneOWrk1K5yR+rQUql6jVyiUdhfS1ULUrJlh3Avh0EhEUc0I6Z/YyMITpztUmu9BoV09K7jMFwHK/RAU+cvFbDIovN4cKkbbCdjt5FFIyBB278dLjrAb+EWOLmoLVbIKICB47AU+8ZSV1SbTrYGUcD0QKBgQCAliZv4na6sg9ZiUPAr+QsKserNSiN5zFkULOPBKLRQbFFbPS1l12pRgLqNCu1qQV19H5tt6arSRpSfy5FB14gFxV4s23yFrnDyF2h2GsFH+MpEq1bbaI1A10AvUnQ5AeKQemRpxPmM2DldMK/H5tPzO0WAOoy4r/ATkc4sG4kxQKBgBL9neT0TmJtxlYGzjNcjdJXs3Q91+nZt3DRMGT9s0917SuP77+FdJYocDiH1rVa9sGG8rkh1jTdqliAxDXwIm5IGS/0OBnkaN1nnGDk5yTiYxOutC5NSj7ecI5Erud8swW6iGqgz2ioFpGxxIYqRlgTv/6mVt41KALfKrYIkVLw \ No newline at end of file diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestFilter.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestFilter.java index 93834a53fb41e..959265accfed3 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestFilter.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestFilter.java @@ -15,7 +15,7 @@ public interface OidcRequestFilter { * Filter OIDC requests * * @param request HTTP request that can have its headers customized - * @param body request body, will be null for HTTP GET methods, may be null for other HTTP methods + * @param requestBody request body, will be null for HTTP GET methods, may be null for other HTTP methods * @param contextProperties context properties that can be available in context of some requests */ void filter(HttpRequest request, Buffer requestBody, OidcRequestContextProperties contextProperties); diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index 2da30b8da5bf5..c16645774ba65 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -274,6 +274,14 @@ public static enum Source { @ConfigItem public Provider secretProvider = new Provider(); + /** + * String representation of a private key. If provided, indicates that JWT is signed using a private key in PEM or + * JWK format. + * You can use the {@link #signatureAlgorithm} property to override the default key algorithm, `RS256`. + */ + @ConfigItem + public Optional key = Optional.empty(); + /** * If provided, indicates that JWT is signed using a private key in PEM or JWK format. * You can use the {@link #signatureAlgorithm} property to override the default key algorithm, `RS256`. @@ -399,6 +407,14 @@ public void setAudience(String audience) { this.audience = Optional.of(audience); } + public Optional getKey() { + return key; + } + + public void setKey(String key) { + this.key = Optional.of(key); + } + public Optional getKeyFile() { return keyFile; } diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java index 6997a29ec767c..fa855e47ec827 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java @@ -280,8 +280,8 @@ public static boolean isClientSecretBasicAuthRequired(Credentials creds) { } public static boolean isClientJwtAuthRequired(Credentials creds) { - return creds.jwt.secret.isPresent() || creds.jwt.secretProvider.key.isPresent() || creds.jwt.keyFile.isPresent() - || creds.jwt.keyStoreFile.isPresent(); + return creds.jwt.secret.isPresent() || creds.jwt.secretProvider.key.isPresent() || creds.jwt.key.isPresent() + || creds.jwt.keyFile.isPresent() || creds.jwt.keyStoreFile.isPresent(); } public static boolean isClientSecretPostAuthRequired(Credentials creds) { @@ -329,7 +329,10 @@ public static Key clientJwtKey(Credentials creds) { } else { Key key = null; try { - if (creds.jwt.getKeyFile().isPresent()) { + if (creds.jwt.getKey().isPresent()) { + key = KeyUtils.tryAsPemSigningPrivateKey(creds.jwt.getKey().get(), + getSignatureAlgorithm(creds, SignatureAlgorithm.RS256)); + } else if (creds.jwt.getKeyFile().isPresent()) { key = KeyUtils.readSigningKey(creds.jwt.getKeyFile().get(), creds.jwt.keyId.orElse(null), getSignatureAlgorithm(creds, SignatureAlgorithm.RS256)); } else if (creds.jwt.keyStoreFile.isPresent()) { From 171a79381d0e75ad14574df3b9c8573fae29537d Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Tue, 16 Apr 2024 16:27:35 +0100 Subject: [PATCH 62/72] Bump smallrye-jwt version to 4.5.1 --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 9449c48011b9c..01f321598bae5 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -57,7 +57,7 @@ 3.10.0 2.8.2 6.3.0 - 4.5.0 + 4.5.1 2.1.0 1.0.13 3.0.1 From 3315867ebfdc4c4c8a3d05d97346f839d446108a Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Wed, 17 Apr 2024 10:42:43 +0200 Subject: [PATCH 63/72] Exclude wallpapers from the preview --- .github/workflows/preview.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index ba6d5146d4d6d..32208ac9fb5d6 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -90,6 +90,7 @@ jobs: find assets/images/posts/ -mindepth 1 -maxdepth 1 -type d -mtime +100 -exec rm -rf _site/{} \; find newsletter/ -mindepth 1 -maxdepth 1 -type d -mtime +100 -exec rm -rf _site/{} \; rm -rf _site/assets/images/worldtour/2023 + rm -rf _site/assets/images/desktopwallpapers - name: Publishing to surge for preview id: deploy From 8df6fcb08bd27d2966138af2f01297d7a39f403e Mon Sep 17 00:00:00 2001 From: Sanne Grinovero Date: Wed, 17 Apr 2024 12:09:51 +0100 Subject: [PATCH 64/72] Remove support for the SecurityManager --- .../security/ldap/DelegatingLdapContext.java | 13 ++++------ .../ldap/QuarkusDirContextFactory.java | 11 +++----- .../ArcConstraintValidatorFactoryImpl.java | 13 +--------- .../runtime/QuarkusRestClientBuilder.java | 13 ++-------- .../spi/QuarkusClassloadingService.java | 12 +++------ .../quarkus/bootstrap/util/PropertyUtils.java | 26 ++----------------- .../registry/config/PropertiesUtil.java | 26 ++----------------- 7 files changed, 19 insertions(+), 95 deletions(-) diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java index c90a9da278e2c..6f43ceb731a5a 100644 --- a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java @@ -1,7 +1,5 @@ package io.quarkus.elytron.security.ldap; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Hashtable; import javax.naming.Binding; @@ -26,7 +24,6 @@ import org.wildfly.common.Assert; import org.wildfly.security.auth.realm.ldap.ThreadLocalSSLSocketFactory; -import org.wildfly.security.manager.action.SetContextClassLoaderAction; class DelegatingLdapContext implements LdapContext { @@ -46,7 +43,7 @@ interface CloseHandler { } // for needs of newInstance() - private DelegatingLdapContext(DirContext delegating, SocketFactory socketFactory) throws NamingException { + private DelegatingLdapContext(DirContext delegating, SocketFactory socketFactory) { this.delegating = delegating; this.closeHandler = null; // close handler should not be applied to copy this.socketFactory = socketFactory; @@ -488,10 +485,10 @@ private ClassLoader getSocketFactoryClassLoader() { } private ClassLoader setClassLoaderTo(final ClassLoader targetClassLoader) { - return doPrivileged(new SetContextClassLoaderAction(targetClassLoader)); + final Thread currentThread = Thread.currentThread(); + final ClassLoader original = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(targetClassLoader); + return original; } - private static T doPrivileged(final PrivilegedAction action) { - return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run(); - } } diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java index 36118d8864f6a..1fe3324d0aa50 100644 --- a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java @@ -1,7 +1,5 @@ package io.quarkus.elytron.security.ldap; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.time.Duration; import java.util.Hashtable; @@ -15,7 +13,6 @@ import javax.security.auth.callback.PasswordCallback; import org.wildfly.security.auth.realm.ldap.DirContextFactory; -import org.wildfly.security.manager.action.SetContextClassLoaderAction; public class QuarkusDirContextFactory implements DirContextFactory { // private static final ElytronMessages log = Logger.getMessageLogger(ElytronMessages.class, "org.wildfly.security"); @@ -142,10 +139,10 @@ public void returnContext(DirContext context) { } private ClassLoader setClassLoaderTo(final ClassLoader targetClassLoader) { - return doPrivileged(new SetContextClassLoaderAction(targetClassLoader)); + final Thread currentThread = Thread.currentThread(); + final ClassLoader original = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(targetClassLoader); + return original; } - private static T doPrivileged(final PrivilegedAction action) { - return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run(); - } } diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java index 6662d05dfb3f7..174ce705d62af 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java @@ -1,7 +1,5 @@ package io.quarkus.hibernate.validator.runtime; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.IdentityHashMap; import java.util.Map; @@ -34,7 +32,7 @@ public class ArcConstraintValidatorFactoryImpl implements ConstraintValidatorFac } return instance; } - return run(NewInstance.action(key, "ConstraintValidator")); + return NewInstance.action(key, "ConstraintValidator").run(); } @Override @@ -45,13 +43,4 @@ public void releaseInstance(ConstraintValidator instance) { } } - /** - * Runs the given privileged action, using a privileged block if required. - *

- * NOTE: This must never be changed into a publicly available method to avoid execution of arbitrary - * privileged actions within HV's protection domain. - */ - private T run(PrivilegedAction action) { - return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run(); - } } diff --git a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java index ead9b44944922..7ecc462c52d12 100644 --- a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java +++ b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java @@ -17,11 +17,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.security.AccessController; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; -import java.security.PrivilegedAction; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; @@ -397,7 +395,7 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi * @return list of proxy hosts */ private List getProxyHostsAsRegex() { - String noProxyHostsSysProps = getSystemProperty("http.nonProxyHosts", null); + String noProxyHostsSysProps = System.getProperty("http.nonProxyHosts", null); if (noProxyHostsSysProps == null) { noProxyHostsSysProps = "localhost|127.*|[::1]"; } else { @@ -414,7 +412,7 @@ private List getProxyHostsAsRegex() { */ private boolean useURLConnection() { if (useURLConnection == null) { - String defaultToURLConnection = getSystemProperty( + String defaultToURLConnection = System.getProperty( "org.jboss.resteasy.microprofile.defaultToURLConnectionHttpClient", "false"); useURLConnection = defaultToURLConnection.equalsIgnoreCase("true"); } @@ -820,13 +818,6 @@ private static BeanManager getBeanManager() { } } - private String getSystemProperty(String key, String def) { - if (System.getSecurityManager() == null) { - return System.getProperty(key, def); - } - return AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty(key, def)); - } - private final MpClientBuilderImpl builderDelegate; private final ConfigurationWrapper configurationWrapper; diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java index 8fc89fdeb99c4..359f9c4417aaa 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java @@ -1,9 +1,5 @@ package io.quarkus.smallrye.graphql.runtime.spi; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; - import graphql.schema.PropertyDataFetcherHelper; import io.smallrye.graphql.execution.Classes; import io.smallrye.graphql.spi.ClassloadingService; @@ -38,12 +34,10 @@ public Class loadClass(String className) { if (Classes.isPrimitive(className)) { return Classes.getPrimativeClassType(className); } else { - return AccessController.doPrivileged((PrivilegedExceptionAction>) () -> { - ClassLoader cl = classLoader == null ? Thread.currentThread().getContextClassLoader() : classLoader; - return loadClass(className, cl); - }); + ClassLoader cl = classLoader == null ? Thread.currentThread().getContextClassLoader() : classLoader; + return loadClass(className, cl); } - } catch (PrivilegedActionException | ClassNotFoundException pae) { + } catch (ClassNotFoundException pae) { throw new RuntimeException("Can not load class [" + className + "]", pae); } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java index 5a15d4b205b38..8751c22210ce4 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java @@ -5,8 +5,6 @@ import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -40,32 +38,12 @@ public static String getUserHome() { public static String getProperty(final String name, String defValue) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name, defValue); - } - }); - } else { - return System.getProperty(name, defValue); - } + return System.getProperty(name, defValue); } public static String getProperty(final String name) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name); - } - }); - } else { - return System.getProperty(name); - } + return System.getProperty(name); } public static final Boolean getBooleanOrNull(String name) { diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java index 57125c8938306..2e4c52b4228f4 100644 --- a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java @@ -1,7 +1,5 @@ package io.quarkus.registry.config; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Locale; public class PropertiesUtil { @@ -26,32 +24,12 @@ public static String getUserHome() { public static String getProperty(final String name, String defValue) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name, defValue); - } - }); - } else { - return System.getProperty(name, defValue); - } + return System.getProperty(name, defValue); } public static String getProperty(final String name) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name); - } - }); - } else { - return System.getProperty(name); - } + return System.getProperty(name); } public static final Boolean getBooleanOrNull(String name) { From e11a8b52b7b5b12feb0ff17f019ccde6945b0526 Mon Sep 17 00:00:00 2001 From: asjervanasten Date: Sun, 3 Mar 2024 21:07:57 +0100 Subject: [PATCH 65/72] Allows to use specific datasource credentials for Liquibase Fixes https://github.com/quarkusio/quarkus/issues/31214 --- .../test/LiquibaseExtensionConfigFixture.java | 8 ++++ .../quarkus/liquibase/LiquibaseFactory.java | 21 ++++++++- .../liquibase/runtime/LiquibaseConfig.java | 12 +++++ .../liquibase/runtime/LiquibaseCreator.java | 2 + .../LiquibaseDataSourceRuntimeConfig.java | 14 ++++++ .../LiquibaseFunctionalityResource.java | 47 ++++++++++++++++++- .../src/main/resources/application.properties | 15 +++++- .../main/resources/db/second/changeLog.xml | 9 ++++ .../main/resources/db/second/create-table.xml | 18 +++++++ .../src/main/resources/db/second/initdb.sql | 2 + .../resources/db/second/insert-into-table.xml | 14 ++++++ .../liquibase/LiquibaseFunctionalityTest.java | 13 +++++ 12 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 integration-tests/liquibase/src/main/resources/db/second/changeLog.xml create mode 100644 integration-tests/liquibase/src/main/resources/db/second/create-table.xml create mode 100644 integration-tests/liquibase/src/main/resources/db/second/initdb.sql create mode 100644 integration-tests/liquibase/src/main/resources/db/second/insert-into-table.xml diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigFixture.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigFixture.java index 258c33815d843..1e4f916ab50d9 100644 --- a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigFixture.java +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigFixture.java @@ -121,6 +121,14 @@ public String defaultSchemaName(String datasourceName) { return getStringValue("quarkus.liquibase.%s.default-schema-name", datasourceName); } + public String username(String datasourceName) { + return getStringValue("quarkus.liquibase.%s.username", datasourceName); + } + + public String password(String datasourceName) { + return getStringValue("quarkus.liquibase.%s.password", datasourceName); + } + public String liquibaseCatalogName(String datasourceName) { return getStringValue("quarkus.liquibase.%s.liquibase-catalog-name", datasourceName); } diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java index d26d6d25c480a..db1c460fa1b64 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/LiquibaseFactory.java @@ -2,10 +2,13 @@ import java.io.FileNotFoundException; import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.DriverManager; import java.util.Map; import javax.sql.DataSource; +import io.agroal.api.AgroalDataSource; import io.quarkus.liquibase.runtime.LiquibaseConfig; import io.quarkus.runtime.util.StringUtil; import liquibase.Contexts; @@ -78,8 +81,22 @@ public Liquibase createLiquibase() { try (ResourceAccessor resourceAccessor = resolveResourceAccessor()) { String parsedChangeLog = parseChangeLog(config.changeLog); - Database database = DatabaseFactory.getInstance() - .findCorrectDatabaseImplementation(new JdbcConnection(dataSource.getConnection())); + Database database; + + if (config.username.isPresent() && config.password.isPresent()) { + AgroalDataSource agroalDataSource = dataSource.unwrap(AgroalDataSource.class); + String jdbcUrl = agroalDataSource.getConfiguration().connectionPoolConfiguration() + .connectionFactoryConfiguration().jdbcUrl(); + Connection connection = DriverManager.getConnection(jdbcUrl, config.username.get(), config.password.get()); + + database = DatabaseFactory.getInstance() + .findCorrectDatabaseImplementation( + new JdbcConnection(connection)); + + } else { + database = DatabaseFactory.getInstance() + .findCorrectDatabaseImplementation(new JdbcConnection(dataSource.getConnection())); + } if (database != null) { database.setDatabaseChangeLogLockTableName(config.databaseChangeLogLockTableName); diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java index 3af8105e1c621..de1f2e47a1f36 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseConfig.java @@ -85,4 +85,16 @@ public class LiquibaseConfig { */ public Optional liquibaseTablespaceName = Optional.empty(); + /** + * The username that Liquibase uses to connect to the database. + * If no username is configured, falls back to the datasource username and password. + */ + public Optional username = Optional.empty(); + + /** + * The password that Liquibase uses to connect to the database. + * If no password is configured, falls back to the datasource username and password. + */ + public Optional password = Optional.empty(); + } diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java index c3d20635e8612..a6c06d69fb47a 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseCreator.java @@ -33,6 +33,8 @@ public LiquibaseFactory createLiquibaseFactory(DataSource dataSource, String dat if (liquibaseRuntimeConfig.databaseChangeLogTableName.isPresent()) { config.databaseChangeLogTableName = liquibaseRuntimeConfig.databaseChangeLogTableName.get(); } + config.password = liquibaseRuntimeConfig.password; + config.username = liquibaseRuntimeConfig.username; config.defaultSchemaName = liquibaseRuntimeConfig.defaultSchemaName; config.defaultCatalogName = liquibaseRuntimeConfig.defaultCatalogName; config.liquibaseTablespaceName = liquibaseRuntimeConfig.liquibaseTablespaceName; diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java index 639590f5d5b58..c8cb0d1cf3e56 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseDataSourceRuntimeConfig.java @@ -101,6 +101,20 @@ public static final LiquibaseDataSourceRuntimeConfig defaultConfig() { @ConfigItem public Optional defaultSchemaName = Optional.empty(); + /** + * The username that Liquibase uses to connect to the database. + * If no specific username is configured, falls back to the datasource username and password. + */ + @ConfigItem + public Optional username = Optional.empty(); + + /** + * The password that Liquibase uses to connect to the database. + * If no specific password is configured, falls back to the datasource username and password. + */ + @ConfigItem + public Optional password = Optional.empty(); + /** * The name of the catalog with the liquibase tables. */ diff --git a/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java b/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java index b71310c69e38c..5ac440f938368 100644 --- a/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java +++ b/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java @@ -1,5 +1,8 @@ package io.quarkus.it.liquibase; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -9,6 +12,9 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.WebApplicationException; +import io.agroal.api.AgroalDataSource; +import io.quarkus.agroal.DataSource; +import io.quarkus.liquibase.LiquibaseDataSource; import io.quarkus.liquibase.LiquibaseFactory; import liquibase.Liquibase; import liquibase.changelog.ChangeSet; @@ -21,6 +27,14 @@ public class LiquibaseFunctionalityResource { @Inject LiquibaseFactory liquibaseFactory; + @Inject + @LiquibaseDataSource("second") + LiquibaseFactory liquibaseSecondFactory; + + @Inject + @DataSource("second") + AgroalDataSource dataSource; + @GET @Path("update") public String doUpdateAuto() { @@ -42,6 +56,38 @@ public String doUpdateAuto() { } } + @GET + @Path("updateWithDedicatedUser") + public String updateWithDedicatedUser() { + try (Liquibase liquibase = liquibaseSecondFactory.createLiquibase()) { + liquibase.update(liquibaseSecondFactory.createContexts(), liquibaseSecondFactory.createLabels()); + List status = liquibase.getChangeSetStatuses(liquibaseSecondFactory.createContexts(), + liquibaseSecondFactory.createLabels()); + List changeSets = Objects.requireNonNull(status, + "ChangeSetStatus is null! Database update was not applied"); + return changeSets.stream() + .filter(ChangeSetStatus::getPreviouslyRan) + .map(ChangeSetStatus::getChangeSet) + .map(ChangeSet::getId) + .collect(Collectors.joining(",")); + } catch (Exception ex) { + throw new WebApplicationException(ex.getMessage(), ex); + } + + } + + @GET + @Path("created-by") + public String returnCreatedByUser() throws SQLException { + try (Connection connection = dataSource.getConnection()) { + ResultSet s = connection.createStatement().executeQuery("SELECT CREATEDBY FROM QUARKUS_TABLE WHERE ID = 1"); + if (s.next()) { + return s.getString("CREATEDBY"); + } + return null; + } + } + private void assertCommandScopeResolvesProperly() { try { new CommandScope("dropAll"); @@ -49,5 +95,4 @@ private void assertCommandScopeResolvesProperly() { throw new RuntimeException("Unable to load 'dropAll' via Liquibase's CommandScope", e); } } - } diff --git a/integration-tests/liquibase/src/main/resources/application.properties b/integration-tests/liquibase/src/main/resources/application.properties index f2bc258ff694d..a357b4c04ebf7 100644 --- a/integration-tests/liquibase/src/main/resources/application.properties +++ b/integration-tests/liquibase/src/main/resources/application.properties @@ -2,7 +2,13 @@ quarkus.datasource.db-kind=h2 quarkus.datasource.username=sa quarkus.datasource.password=sa -quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test_quarkus;DB_CLOSE_DELAY=-1 +quarkus.datasource.jdbc.url=jdbc:h2:mem:test + +# Second datasource +quarkus.datasource.second.db-kind=h2 +quarkus.datasource.second.username=sa +quarkus.datasource.second.password=sa +quarkus.datasource.second.jdbc.url=jdbc:h2:mem:second;INIT=RUNSCRIPT FROM 'src/main/resources/db/second/initdb.sql' # Liquibase config properties quarkus.liquibase.change-log=db/changeLog.xml @@ -11,6 +17,13 @@ quarkus.liquibase.migrate-at-start=false quarkus.liquibase.database-change-log-lock-table-name=changelog_lock quarkus.liquibase.database-change-log-table-name=changelog +# Config for second datasource with different user / password +quarkus.liquibase.second.username=usr +quarkus.liquibase.second.password=pass +quarkus.liquibase.second.change-log=db/second/changeLog.xml +quarkus.liquibase.second.clean-at-start=false +quarkus.liquibase.second.migrate-at-start=false + # Debug logging #quarkus.log.console.level=DEBUG #quarkus.log.category."liquibase".level=DEBUG diff --git a/integration-tests/liquibase/src/main/resources/db/second/changeLog.xml b/integration-tests/liquibase/src/main/resources/db/second/changeLog.xml new file mode 100644 index 0000000000000..8d79230fa4d32 --- /dev/null +++ b/integration-tests/liquibase/src/main/resources/db/second/changeLog.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/integration-tests/liquibase/src/main/resources/db/second/create-table.xml b/integration-tests/liquibase/src/main/resources/db/second/create-table.xml new file mode 100644 index 0000000000000..7878e39dd51fd --- /dev/null +++ b/integration-tests/liquibase/src/main/resources/db/second/create-table.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integration-tests/liquibase/src/main/resources/db/second/initdb.sql b/integration-tests/liquibase/src/main/resources/db/second/initdb.sql new file mode 100644 index 0000000000000..ba7afae2a27d8 --- /dev/null +++ b/integration-tests/liquibase/src/main/resources/db/second/initdb.sql @@ -0,0 +1,2 @@ +CREATE USER IF NOT EXISTS usr PASSWORD 'pass' ADMIN; +GRANT ALL ON SCHEMA PUBLIC TO usr; \ No newline at end of file diff --git a/integration-tests/liquibase/src/main/resources/db/second/insert-into-table.xml b/integration-tests/liquibase/src/main/resources/db/second/insert-into-table.xml new file mode 100644 index 0000000000000..60c8d153f099a --- /dev/null +++ b/integration-tests/liquibase/src/main/resources/db/second/insert-into-table.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java b/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java index 52246e1254d69..f2b7ccfe6a5ad 100644 --- a/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java +++ b/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java @@ -5,6 +5,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import io.quarkus.test.junit.QuarkusTest; @@ -18,6 +20,17 @@ public void testLiquibaseQuarkusFunctionality() { doTestLiquibaseQuarkusFunctionality(isIncludeAllExpectedToWork()); } + @Test + @DisplayName("Migrates a schema correctly using dedicated username and password from config properties") + @DisabledOnOs(value = OS.WINDOWS, disabledReason = "Our Windows CI does not have Docker installed properly") + public void testLiquibaseUsingDedicatedUsernameAndPassword() { + when().get("/liquibase/updateWithDedicatedUser").then().body(is( + "create-quarkus-table,insert-into-quarkus-table")); + + when().get("/liquibase/created-by").then().body(is( + "USR")); + } + static void doTestLiquibaseQuarkusFunctionality(boolean isIncludeAllExpectedToWork) { when() .get("/liquibase/update") From 0258098f5a69be915a6b74d6a14b08422d6ed7fc Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Wed, 17 Apr 2024 16:04:12 +0200 Subject: [PATCH 66/72] Adjust the new Liquibase tests a bit Not sure exactly what was going wrong but I had to create two users to make things work and also make sure everything was done in one go. --- .../LiquibaseFunctionalityResource.java | 32 ++++++++----------- .../src/main/resources/application.properties | 8 ++--- .../src/main/resources/db/second/initdb.sql | 7 ++-- .../liquibase/LiquibaseFunctionalityTest.java | 8 +---- 4 files changed, 23 insertions(+), 32 deletions(-) diff --git a/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java b/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java index 5ac440f938368..501fbdb58a82f 100644 --- a/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java +++ b/integration-tests/liquibase/src/main/java/io/quarkus/it/liquibase/LiquibaseFunctionalityResource.java @@ -2,7 +2,7 @@ import java.sql.Connection; import java.sql.ResultSet; -import java.sql.SQLException; +import java.sql.Statement; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -33,7 +33,7 @@ public class LiquibaseFunctionalityResource { @Inject @DataSource("second") - AgroalDataSource dataSource; + AgroalDataSource secondDataSource; @GET @Path("update") @@ -46,6 +46,7 @@ public String doUpdateAuto() { liquibaseFactory.createLabels()); List changeSets = Objects.requireNonNull(status, "ChangeSetStatus is null! Database update was not applied"); + return changeSets.stream() .filter(ChangeSetStatus::getPreviouslyRan) .map(ChangeSetStatus::getChangeSet) @@ -65,29 +66,22 @@ public String updateWithDedicatedUser() { liquibaseSecondFactory.createLabels()); List changeSets = Objects.requireNonNull(status, "ChangeSetStatus is null! Database update was not applied"); - return changeSets.stream() - .filter(ChangeSetStatus::getPreviouslyRan) - .map(ChangeSetStatus::getChangeSet) - .map(ChangeSet::getId) - .collect(Collectors.joining(",")); + + try (Connection connection = secondDataSource.getConnection()) { + try (Statement s = connection.createStatement()) { + ResultSet rs = s.executeQuery("SELECT CREATEDBY FROM QUARKUS_TABLE WHERE ID = 1"); + if (rs.next()) { + return rs.getString("CREATEDBY"); + } + return null; + } + } } catch (Exception ex) { throw new WebApplicationException(ex.getMessage(), ex); } } - @GET - @Path("created-by") - public String returnCreatedByUser() throws SQLException { - try (Connection connection = dataSource.getConnection()) { - ResultSet s = connection.createStatement().executeQuery("SELECT CREATEDBY FROM QUARKUS_TABLE WHERE ID = 1"); - if (s.next()) { - return s.getString("CREATEDBY"); - } - return null; - } - } - private void assertCommandScopeResolvesProperly() { try { new CommandScope("dropAll"); diff --git a/integration-tests/liquibase/src/main/resources/application.properties b/integration-tests/liquibase/src/main/resources/application.properties index a357b4c04ebf7..036f9cef122dc 100644 --- a/integration-tests/liquibase/src/main/resources/application.properties +++ b/integration-tests/liquibase/src/main/resources/application.properties @@ -2,12 +2,12 @@ quarkus.datasource.db-kind=h2 quarkus.datasource.username=sa quarkus.datasource.password=sa -quarkus.datasource.jdbc.url=jdbc:h2:mem:test +quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1 # Second datasource quarkus.datasource.second.db-kind=h2 -quarkus.datasource.second.username=sa -quarkus.datasource.second.password=sa +quarkus.datasource.second.username=readonly +quarkus.datasource.second.password=readonly quarkus.datasource.second.jdbc.url=jdbc:h2:mem:second;INIT=RUNSCRIPT FROM 'src/main/resources/db/second/initdb.sql' # Liquibase config properties @@ -18,7 +18,7 @@ quarkus.liquibase.database-change-log-lock-table-name=changelog_lock quarkus.liquibase.database-change-log-table-name=changelog # Config for second datasource with different user / password -quarkus.liquibase.second.username=usr +quarkus.liquibase.second.username=admin quarkus.liquibase.second.password=pass quarkus.liquibase.second.change-log=db/second/changeLog.xml quarkus.liquibase.second.clean-at-start=false diff --git a/integration-tests/liquibase/src/main/resources/db/second/initdb.sql b/integration-tests/liquibase/src/main/resources/db/second/initdb.sql index ba7afae2a27d8..f1f2c732613c6 100644 --- a/integration-tests/liquibase/src/main/resources/db/second/initdb.sql +++ b/integration-tests/liquibase/src/main/resources/db/second/initdb.sql @@ -1,2 +1,5 @@ -CREATE USER IF NOT EXISTS usr PASSWORD 'pass' ADMIN; -GRANT ALL ON SCHEMA PUBLIC TO usr; \ No newline at end of file +CREATE USER IF NOT EXISTS admin PASSWORD 'pass' ADMIN; +GRANT ALL ON SCHEMA PUBLIC TO admin; + +CREATE USER IF NOT EXISTS readonly PASSWORD 'readonly' ADMIN; +GRANT SELECT ON SCHEMA PUBLIC TO readonly; \ No newline at end of file diff --git a/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java b/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java index f2b7ccfe6a5ad..447537cdef628 100644 --- a/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java +++ b/integration-tests/liquibase/src/test/java/io/quarkus/it/liquibase/LiquibaseFunctionalityTest.java @@ -5,8 +5,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.junit.jupiter.api.condition.OS; import io.quarkus.test.junit.QuarkusTest; @@ -22,13 +20,9 @@ public void testLiquibaseQuarkusFunctionality() { @Test @DisplayName("Migrates a schema correctly using dedicated username and password from config properties") - @DisabledOnOs(value = OS.WINDOWS, disabledReason = "Our Windows CI does not have Docker installed properly") public void testLiquibaseUsingDedicatedUsernameAndPassword() { when().get("/liquibase/updateWithDedicatedUser").then().body(is( - "create-quarkus-table,insert-into-quarkus-table")); - - when().get("/liquibase/created-by").then().body(is( - "USR")); + "ADMIN")); } static void doTestLiquibaseQuarkusFunctionality(boolean isIncludeAllExpectedToWork) { From 71ee011a0e70813eacd5cffcc9dbd988fef9d0f5 Mon Sep 17 00:00:00 2001 From: Selim Date: Sat, 16 Mar 2024 23:44:15 +0300 Subject: [PATCH 67/72] update default values for salt & iteration index --- docs/src/main/asciidoc/security-jdbc.adoc | 6 +----- .../security/jdbc/BcryptPasswordKeyMapperConfig.java | 10 ++++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/src/main/asciidoc/security-jdbc.adoc b/docs/src/main/asciidoc/security-jdbc.adoc index 7a3e8a906f15d..fba33d27df325 100644 --- a/docs/src/main/asciidoc/security-jdbc.adoc +++ b/docs/src/main/asciidoc/security-jdbc.adoc @@ -209,8 +209,6 @@ quarkus.security.jdbc.enabled=true quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM test_user u WHERE u.username=? <1> quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true <2> quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index=-1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-index=-1 quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2 <3> quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups ---- @@ -218,7 +216,7 @@ quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups The `elytron-security-jdbc` extension requires at least one principal query to authenticate the user and its identity. <1> We define a parameterized SQL statement (with exactly 1 parameter) which should return the user's password plus any additional information you want to load. -<2> We configure the password mapper with the position of the password field in the `SELECT` fields and other information like salt, hash encoding, etc. Setting the salt and iteration count indexes to `-1` is required for MCF. +<2> The password mapper is configured with the position of the password field in the `SELECT` fields. The hash is stored in the Modular Crypt Format (MCF) because the salt and iteration count indexes are set to `-1` by default. You can override them in order to decompose each element into three separate columns. <3> We use `attribute-mappings` to bind the `SELECT` projection fields (i.e. `u.role` here) to the target Principal representation attributes. [NOTE] @@ -311,8 +309,6 @@ quarkus.security.jdbc.enabled=true quarkus.security.jdbc.principal-query.sql=SELECT u.password FROM test_user u WHERE u.username=? quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index=-1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-index=-1 quarkus.security.jdbc.principal-query.roles.sql=SELECT r.role_name FROM test_role r, test_user_role ur WHERE ur.username=? AND ur.role_id = r.id quarkus.security.jdbc.principal-query.roles.datasource=permissions diff --git a/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java b/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java index 5848f998f5836..d7c43a70a89f6 100644 --- a/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java +++ b/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java @@ -34,9 +34,10 @@ public interface BcryptPasswordKeyMapperConfig { Encoding hashEncoding(); /** - * The index (1 based numbering) of the column containing the Bcrypt salt + * The index (1 based numbering) of the column containing the Bcrypt salt. The default value of `-1` implies that the salt + * is stored in the password column using the Modular Crypt Format (MCF) standard. */ - @WithDefault("0") + @WithDefault("-1") int saltIndex(); /** @@ -46,9 +47,10 @@ public interface BcryptPasswordKeyMapperConfig { Encoding saltEncoding(); /** - * The index (1 based numbering) of the column containing the Bcrypt iteration count + * The index (1 based numbering) of the column containing the Bcrypt iteration count. The default value of `-1` implies that + * the iteration count is stored in the password column using the Modular Crypt Format (MCF) standard. */ - @WithDefault("0") + @WithDefault("-1") int iterationCountIndex(); default PasswordKeyMapper toPasswordKeyMapper() { From b4d152ef2a5e8020ce3b7672a233f31443920c9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 19:55:00 +0000 Subject: [PATCH 68/72] Bump com.gradle.develocity from 3.17.1 to 3.17.2 in /devtools/gradle Bumps com.gradle.develocity from 3.17.1 to 3.17.2. --- updated-dependencies: - dependency-name: com.gradle.develocity dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- devtools/gradle/settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/gradle/settings.gradle.kts b/devtools/gradle/settings.gradle.kts index 50427c0789cc1..8871081257ab4 100644 --- a/devtools/gradle/settings.gradle.kts +++ b/devtools/gradle/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.gradle.develocity") version "3.17.1" + id("com.gradle.develocity") version "3.17.2" } develocity { From fc126279f63418949a9b903d009884ccee8a7f3f Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 18 Apr 2024 08:34:07 +0200 Subject: [PATCH 69/72] Make test release workflow more consistent with release --- .github/workflows/release-build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 33adbbdecaa23..c7ebbe6f2f743 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -30,10 +30,14 @@ jobs: run: | ./mvnw --settings .github/mvn-settings.xml \ -B \ + -Dscan=false \ + -Dno-build-cache \ + -Dgradle.cache.local.enabled=false \ + -Dgradle.cache.remote.enabled=false \ -Prelease \ -DskipTests -DskipITs \ -Ddokka \ - -Dmaven.repo.local=$HOME/release/repository \ + -Dno-test-modules \ -Dgpg.skip \ clean install - name: Report From e44d7a70e4dd19e939a4ee2d312364479909d431 Mon Sep 17 00:00:00 2001 From: Thomas Segismont Date: Thu, 18 Apr 2024 11:12:48 +0200 Subject: [PATCH 70/72] Update kafka doc: Emitter `Emitter` is not an annotation. --- docs/src/main/asciidoc/kafka.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index a9d105aa244ab..ff532b0c198ab 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -1017,7 +1017,7 @@ In this case the producer will use this method as generator to create an infinit @Outgoing("prices-out") CompletionStage> generate(); ---- -=== Sending messages with @Emitter +=== Sending messages with Emitter Sometimes, you need to have an imperative way of sending messages. From 9840da101ce9e351b433b5112bc2a9148e5069e3 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 18 Apr 2024 12:58:08 +0200 Subject: [PATCH 71/72] Qute type-safe messages: add convenient way to localize enum constants - resolves #40089 --- docs/src/main/asciidoc/qute-reference.adoc | 34 +++ .../MessageBundleMethodBuildItem.java | 31 +- .../deployment/MessageBundleProcessor.java | 281 ++++++++++++++++-- .../qute/deployment/QuteProcessor.java | 14 +- .../i18n/MessageBundleEnumTest.java | 74 +++++ .../i18n/MessageBundleLogicalLineTest.java | 10 +- .../deployment/i18n/MessageBundleTest.java | 2 +- .../quarkus/qute/deployment/i18n/MyEnum.java | 10 + .../test/resources/messages/enu.properties | 13 + .../test/resources/messages/enu_cs.properties | 13 + .../java/io/quarkus/qute/i18n/Message.java | 29 +- 11 files changed, 462 insertions(+), 49 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java create mode 100644 extensions/qute/deployment/src/test/resources/messages/enu.properties create mode 100644 extensions/qute/deployment/src/test/resources/messages/enu_cs.properties diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index dea89afaf0357..8b31a7c50f139 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -2858,6 +2858,40 @@ public class MyBean { ---- <1> The annotation value is a locale tag string (IETF). +===== Enums + +There is a convenient way to localize enums. +If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined: + +[source,java] +---- +@Message <1> +String methodName(MyEnum enum); +---- +<1> The value is intentionally not provided. There's also no key for the method in a localized file. + +Then it receives a generated template: + +[source,html] +---- +{#when enumParamName} + {#is CONSTANT1}{msg:methodName_CONSTANT1} + {#is CONSTANT2}{msg:methodName_CONSTANT2} +{/when} +---- + +Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and values for all constant message keys: + +[source,poperties] +---- +methodName_CONSTANT1=Value 1 +methodName_CONSTANT2=Value 2 +---- + +In a template, an enum constant can be localized with a message bundle method like `{msg:methodName(enumConstant)}`. + +TIP: There is also <> - a convenient annotation to access enum constants in a template. + ==== Message Templates Every method of a message bundle interface must define a message template. The value is normally defined by `io.quarkus.qute.i18n.Message#value()`, diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java index 3509d439e5821..56809719f7b0a 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java @@ -3,11 +3,12 @@ import org.jboss.jandex.MethodInfo; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; /** * Represents a message bundle method. *

- * Note that templates that contain no expressions don't need to be validated. + * Note that templates that contain no expressions/sections don't need to be validated. */ public final class MessageBundleMethodBuildItem extends MultiBuildItem { @@ -36,14 +37,27 @@ public String getKey() { return key; } + /** + * + * @return the template id or {@code null} if there is no need to use qute; i.e. no expression/section found + */ public String getTemplateId() { return templateId; } + /** + * For example, there is no corresponding method for generated enum constant message keys. + * + * @return the method or {@code null} if there is no corresponding method declared on the message bundle interface + */ public MethodInfo getMethod() { return method; } + public boolean hasMethod() { + return method != null; + } + public String getTemplate() { return template; } @@ -65,4 +79,19 @@ public boolean isDefaultBundle() { return isDefaultBundle; } + /** + * + * @return the path + * @see TemplateAnalysis#path + */ + public String getPathForAnalysis() { + if (method != null) { + return method.declaringClass().name() + "#" + method.name(); + } + if (templateId != null) { + return templateId; + } + return bundleName + "_" + key; + } + } 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 56e4529393b9b..3b45e64f29e94 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 @@ -38,6 +38,7 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassInfo.NestingType; 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; @@ -85,6 +86,8 @@ import io.quarkus.qute.Namespaces; import io.quarkus.qute.Resolver; import io.quarkus.qute.SectionHelperFactory; +import io.quarkus.qute.TemplateException; +import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.deployment.QuteProcessor.JavaMemberLookupConfig; import io.quarkus.qute.deployment.QuteProcessor.MatchResult; import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; @@ -266,7 +269,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv // Generate implementations // name -> impl class Map generatedImplementations = generateImplementations(bundles, generatedClasses, - messageTemplateMethods); + messageTemplateMethods, index); // Register synthetic beans for (MessageBundleBuildItem bundle : bundles) { @@ -393,8 +396,11 @@ void validateMessageBundleMethods(TemplatesAnalysisBuildItem templatesAnalysis, if (messageBundleMethod != null) { // All top-level expressions without a namespace should be mapped to a param Set usedParamNames = new HashSet<>(); - Set paramNames = IntStream.range(0, messageBundleMethod.getMethod().parametersCount()) - .mapToObj(idx -> getParameterName(messageBundleMethod.getMethod(), idx)).collect(Collectors.toSet()); + Set paramNames = messageBundleMethod.hasMethod() + ? IntStream.range(0, messageBundleMethod.getMethod().parametersCount()) + .mapToObj(idx -> getParameterName(messageBundleMethod.getMethod(), idx)) + .collect(Collectors.toSet()) + : Set.of(); for (Expression expression : analysis.expressions) { validateExpression(incorrectExpressions, messageBundleMethod, expression, paramNames, usedParamNames, globals); @@ -431,9 +437,8 @@ private void validateExpression(BuildProducer inco // Expression has no type info or type info that does not match a method parameter // expressions that have incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), - name + " is not a parameter of the message bundle method " - + messageBundleMethod.getMethod().declaringClass().name() + "#" - + messageBundleMethod.getMethod().name() + "()", + name + " is not a parameter of the message bundle method: " + + messageBundleMethod.getPathForAnalysis(), expression.getOrigin())); } else { usedParamNames.add(name); @@ -568,6 +573,10 @@ public String apply(String id) { MethodInfo method = methods.get(methodPart.getName()); if (method == null) { + if (methods.containsKey(methodPart.getName())) { + // Skip validation - enum constant key + continue; + } if (!methodPart.isVirtualMethod() || methodPart.asVirtualMethod().getParameters().isEmpty()) { // The method template may contain no expressions method = defaultBundleInterface.method(methodPart.getName()); @@ -690,7 +699,8 @@ void generateExamplePropertiesFiles(List messageBu private Map generateImplementations(List bundles, BuildProducer generatedClasses, - BuildProducer messageTemplateMethods) throws IOException { + BuildProducer messageTemplateMethods, + IndexView index) throws IOException { Map generatedTypes = new HashMap<>(); @@ -701,29 +711,33 @@ private Map generateImplementations(List // take message templates not specified by Message#value from corresponding localized file Map defaultKeyToMap = getLocalizedFileKeyToTemplate(bundle, bundleInterface, - bundle.getDefaultLocale(), bundleInterface.methods(), null); + bundle.getDefaultLocale(), bundleInterface.methods(), null, index); MergeClassInfoWrapper bundleInterfaceWrapper = new MergeClassInfoWrapper(bundleInterface, null, null); + // Generate implementation for the default bundle interface String bundleImpl = generateImplementation(bundle, null, null, bundleInterfaceWrapper, - defaultClassOutput, messageTemplateMethods, defaultKeyToMap, null); + defaultClassOutput, messageTemplateMethods, defaultKeyToMap, null, index); generatedTypes.put(bundleInterface.name().toString(), bundleImpl); + + // Generate imeplementation for each localized interface for (Entry entry : bundle.getLocalizedInterfaces().entrySet()) { ClassInfo localizedInterface = entry.getValue(); // take message templates not specified by Message#value from corresponding localized file Map keyToMap = getLocalizedFileKeyToTemplate(bundle, bundleInterface, entry.getKey(), - localizedInterface.methods(), localizedInterface); + localizedInterface.methods(), localizedInterface, index); MergeClassInfoWrapper localizedInterfaceWrapper = new MergeClassInfoWrapper(localizedInterface, bundleInterface, keyToMap); generatedTypes.put(entry.getValue().name().toString(), generateImplementation(bundle, bundleInterface, bundleImpl, localizedInterfaceWrapper, - defaultClassOutput, messageTemplateMethods, keyToMap, null)); + defaultClassOutput, messageTemplateMethods, keyToMap, null, index)); } + // Generate implementation for each localized file for (Entry entry : bundle.getLocalizedFiles().entrySet()) { Path localizedFile = entry.getValue(); - var keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile); + var keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile, index); String locale = entry.getKey(); ClassOutput localeAwareGizmoAdaptor = new GeneratedClassGizmoAdaptor(generatedClasses, @@ -739,19 +753,19 @@ public String apply(String className) { })); generatedTypes.put(localizedFile.toString(), generateImplementation(bundle, bundleInterface, bundleImpl, new SimpleClassInfoWrapper(bundleInterface), - localeAwareGizmoAdaptor, messageTemplateMethods, keyToTemplate, locale)); + localeAwareGizmoAdaptor, messageTemplateMethods, keyToTemplate, locale, index)); } } return generatedTypes; } private Map getLocalizedFileKeyToTemplate(MessageBundleBuildItem bundle, - ClassInfo bundleInterface, String locale, List methods, ClassInfo localizedInterface) + ClassInfo bundleInterface, String locale, List methods, ClassInfo localizedInterface, IndexView index) throws IOException { Path localizedFile = bundle.getMergeCandidates().get(locale); if (localizedFile != null) { - Map keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile); + Map keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile, index); if (!keyToTemplate.isEmpty()) { // keep message templates if value wasn't provided by Message#value @@ -785,7 +799,7 @@ private Map getLocalizedFileKeyToTemplate(MessageBundleBuildItem } private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundleInterface, - Path localizedFile) throws IOException { + Path localizedFile, IndexView index) throws IOException { Map keyToTemplate = new HashMap<>(); for (ListIterator it = Files.readAllLines(localizedFile).listIterator(); it.hasNext();) { String line = it.next(); @@ -804,7 +818,7 @@ private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundle "Missing key/value separator\n\t- file: " + localizedFile + "\n\t- line " + it.previousIndex()); } String key = line.substring(0, eqIdx).strip(); - if (!hasMessageBundleMethod(bundleInterface, key)) { + if (!hasMessageBundleMethod(bundleInterface, key) && !isEnumConstantMessageKey(key, index, bundleInterface)) { throw new MessageBundleException( "Message bundle method " + key + "() not found on: " + bundleInterface + "\n\t- file: " + localizedFile + "\n\t- line " + it.previousIndex()); @@ -822,6 +836,42 @@ private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundle return keyToTemplate; } + /** + * + * @param key + * @param bundleInterface + * @return {@code true} if the given key represents an enum constant message key, such as {@code myEnum_CONSTANT1} + * @see #toEnumConstantKey(String, String) + */ + boolean isEnumConstantMessageKey(String key, IndexView index, ClassInfo bundleInterface) { + if (key.isBlank()) { + return false; + } + int lastIdx = key.lastIndexOf("_"); + if (lastIdx != -1 && lastIdx != key.length()) { + String methodName = key.substring(0, lastIdx); + String constant = key.substring(lastIdx + 1, key.length()); + MethodInfo method = messageBundleMethod(bundleInterface, methodName); + if (method != null && method.parametersCount() == 1) { + Type paramType = method.parameterType(0); + if (paramType.kind() == org.jboss.jandex.Type.Kind.CLASS) { + ClassInfo maybeEnum = index.getClassByName(paramType.name()); + if (maybeEnum != null && maybeEnum.isEnum()) { + if (maybeEnum.fields().stream() + .filter(FieldInfo::isEnumConstant) + .map(FieldInfo::name) + .anyMatch(constant::equals)) { + return true; + } + throw new MessageBundleException( + String.format("%s is not an enum constant of %: %s", constant, maybeEnum, key)); + } + } + } + } + return false; + } + private void constructLine(StringBuilder builder, Iterator it) { if (it.hasNext()) { String nextLine = adaptLine(it.next()); @@ -839,19 +889,22 @@ private String adaptLine(String line) { } private boolean hasMessageBundleMethod(ClassInfo bundleInterface, String name) { + return messageBundleMethod(bundleInterface, name) != null; + } + + private MethodInfo messageBundleMethod(ClassInfo bundleInterface, String name) { for (MethodInfo method : bundleInterface.methods()) { if (method.name().equals(name)) { - return true; + return method; } } - return false; + return null; } private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo defaultBundleInterface, - String defaultBundleImpl, - ClassInfoWrapper bundleInterfaceWrapper, ClassOutput classOutput, + String defaultBundleImpl, ClassInfoWrapper bundleInterfaceWrapper, ClassOutput classOutput, BuildProducer messageTemplateMethods, - Map messageTemplates, String locale) { + Map messageTemplates, String locale, IndexView index) { ClassInfo bundleInterface = bundleInterfaceWrapper.getClassInfo(); LOG.debugf("Generate bundle implementation for %s", bundleInterface); @@ -884,7 +937,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d ClassCreator bundleCreator = builder.build(); // key -> method - Map keyMap = new LinkedHashMap<>(); + Map keyMap = new LinkedHashMap<>(); List methods = new ArrayList<>(bundleInterfaceWrapper.methods()); // Sort methods methods.sort(Comparator.comparing(MethodInfo::name).thenComparing(Comparator.comparing(MethodInfo::toString))); @@ -927,7 +980,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d if (keyMap.containsKey(key)) { throw new MessageBundleException(String.format("Duplicate key [%s] found on %s", key, bundleInterface)); } - keyMap.put(key, method); + keyMap.put(key, new SimpleMessageMethod(method)); String messageTemplate = messageTemplates.get(method.name()); if (messageTemplate == null) { @@ -940,6 +993,50 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d method.parameterTypes().toArray(new Type[] {}))).annotation(Names.MESSAGE)); } + // We need some special handling for enum message bundle methods + // A message bundle method that accepts an enum and has no message template receives a generated template: + // {#when enumParamName} + // {#is CONSTANT1}{msg:org_acme_MyEnum_CONSTANT1} + // {#is CONSTANT2}{msg:org_acme_MyEnum_CONSTANT2} + // ... + // {/when} + // Furthermore, a special message method is generated for each enum constant + if (messageTemplate == null && method.parametersCount() == 1) { + Type paramType = method.parameterType(0); + if (paramType.kind() == org.jboss.jandex.Type.Kind.CLASS) { + ClassInfo maybeEnum = index.getClassByName(paramType.name()); + if (maybeEnum != null && maybeEnum.isEnum()) { + StringBuilder generatedMessageTemplate = new StringBuilder("{#when ") + .append(getParameterName(method, 0)) + .append("}"); + Set enumConstants = maybeEnum.fields().stream().filter(FieldInfo::isEnumConstant) + .map(FieldInfo::name).collect(Collectors.toSet()); + for (String enumConstant : enumConstants) { + // org_acme_MyEnum_CONSTANT1 + String enumConstantKey = toEnumConstantKey(method.name(), enumConstant); + String enumConstantTemplate = messageTemplates.get(enumConstantKey); + if (enumConstantTemplate == null) { + throw new TemplateException( + String.format("Enum constant message not found in bundle [%s] for key: %s", + bundleName + (locale != null ? "_" + locale : ""), enumConstantKey)); + } + generatedMessageTemplate.append("{#is ") + .append(enumConstant) + .append("}{") + .append(bundle.getName()) + .append(":") + .append(enumConstantKey) + .append("}"); + generateEnumConstantMessageMethod(bundleCreator, bundleName, locale, bundleInterface, + defaultBundleInterface, enumConstantKey, keyMap, enumConstantTemplate, + messageTemplateMethods); + } + generatedMessageTemplate.append("{/when}"); + messageTemplate = generatedMessageTemplate.toString(); + } + } + } + if (messageTemplate == null) { throw new MessageBundleException( String.format("Message template for key [%s] is missing for default locale [%s]", key, @@ -948,6 +1045,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d String templateId = null; if (messageTemplate.contains("}")) { + // Qute is needed - at least one expression/section found if (defaultBundleInterface != null) { if (locale == null) { AnnotationInstance localizedAnnotation = bundleInterface.declaredAnnotation(Names.LOCALIZED); @@ -975,6 +1073,12 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d // Create a template instance ResultHandle templateInstance = bundleMethod .invokeInterfaceMethod(io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE, template); + if (locale != null) { + bundleMethod.invokeInterfaceMethod( + MethodDescriptor.ofMethod(TemplateInstance.class, "setLocale", TemplateInstance.class, + String.class), + templateInstance, bundleMethod.load(locale)); + } List paramTypes = method.parameterTypes(); if (!paramTypes.isEmpty()) { // Set data @@ -1002,6 +1106,62 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d return generatedName.replace('/', '.'); } + private String toEnumConstantKey(String methodName, String enumConstant) { + return methodName + "_" + enumConstant; + } + + private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, String bundleName, String locale, + ClassInfo bundleInterface, ClassInfo defaultBundleInterface, String enumConstantKey, + Map keyMap, String messageTemplate, + BuildProducer messageTemplateMethods) { + String templateId = null; + if (messageTemplate.contains("}")) { + if (defaultBundleInterface != null) { + if (locale == null) { + AnnotationInstance localizedAnnotation = bundleInterface + .declaredAnnotation(Names.LOCALIZED); + locale = localizedAnnotation.value().asString(); + } + templateId = bundleName + "_" + locale + "_" + enumConstantKey; + } else { + templateId = bundleName + "_" + enumConstantKey; + } + } + + MessageBundleMethodBuildItem messageBundleMethod = new MessageBundleMethodBuildItem(bundleName, enumConstantKey, + templateId, null, messageTemplate, + defaultBundleInterface == null); + messageTemplateMethods.produce(messageBundleMethod); + + MethodCreator enumConstantMethod = bundleCreator.getMethodCreator(enumConstantKey, + String.class); + + if (!messageBundleMethod.isValidatable()) { + // No expression/tag - no need to use qute + enumConstantMethod.returnValue(enumConstantMethod.load(messageTemplate)); + } else { + // Obtain the template, e.g. msg_org_acme_MyEnum_CONSTANT1 + ResultHandle template = enumConstantMethod.invokeStaticMethod( + io.quarkus.qute.deployment.Descriptors.BUNDLES_GET_TEMPLATE, + enumConstantMethod.load(templateId)); + // Create a template instance + ResultHandle templateInstance = enumConstantMethod + .invokeInterfaceMethod(io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE, template); + if (locale != null) { + enumConstantMethod.invokeInterfaceMethod( + MethodDescriptor.ofMethod(TemplateInstance.class, "setLocale", TemplateInstance.class, + String.class), + templateInstance, enumConstantMethod.load(locale)); + } + // Render the template + enumConstantMethod.returnValue(enumConstantMethod.invokeInterfaceMethod( + io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE_RENDER, templateInstance)); + } + + keyMap.put(enumConstantKey, + new EnumConstantMessageMethod(enumConstantMethod.getMethodDescriptor())); + } + /** * @return {@link Message#value()} if value was provided */ @@ -1035,7 +1195,7 @@ static String getParameterName(MethodInfo method, int position) { return name; } - private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreator, Map keyMap) { + private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreator, Map keyMap) { MethodCreator resolve = bundleCreator.getMethodCreator("resolve", CompletionStage.class, EvalContext.class); String resolveMethodPrefix = bundleCreator.getClassName().contains("/") ? bundleCreator.getClassName().substring(bundleCreator.getClassName().lastIndexOf('/') + 1) @@ -1106,7 +1266,7 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat int resolveIndex = 0; MethodCreator resolveGroup = null; - for (Entry entry : keyMap.entrySet()) { + for (Entry entry : keyMap.entrySet()) { if (resolveGroup == null || groupIndex++ >= groupLimit) { groupIndex = 0; String resolveMethodName = resolveMethodPrefix + "_resolve_" + resolveIndex++; @@ -1147,16 +1307,18 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat } } - private void addMessageMethod(MethodCreator resolve, String key, MethodInfo method, ResultHandle name, + private void addMessageMethod(MethodCreator resolve, String key, MessageMethod method, ResultHandle name, ResultHandle evaluatedParams, ResultHandle ret, String bundleClass) { List methodParams = method.parameterTypes(); BytecodeCreator matched = resolve.ifTrue(Gizmo.equals(resolve, resolve.load(key), name)) .trueBranch(); - if (method.parameterTypes().isEmpty()) { + if (methodParams.isEmpty()) { matched.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, ret, - matched.invokeInterfaceMethod(method, matched.getThis())); + method.isMessageBundleInterfaceMethod() + ? matched.invokeInterfaceMethod(method.descriptor(), matched.getThis()) + : matched.invokeVirtualMethod(method.descriptor(), matched.getThis())); matched.returnValue(ret); } else { // The CompletionStage upon which we invoke whenComplete() @@ -1200,7 +1362,9 @@ private void addMessageMethod(MethodCreator resolve, String key, MethodInfo meth exception.getCaughtException()); tryCatch.assign(invokeRet, - tryCatch.invokeInterfaceMethod(MethodDescriptor.of(method), whenThis, paramsHandle)); + method.isMessageBundleInterfaceMethod() + ? tryCatch.invokeInterfaceMethod(method.descriptor(), whenThis, paramsHandle) + : tryCatch.invokeVirtualMethod(method.descriptor(), whenThis, paramsHandle)); tryCatch.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, whenRet, invokeRet); // CompletableFuture.completeExceptionally(Throwable) @@ -1424,4 +1588,61 @@ public final MethodInfo method(String name, Type... parameters) { return classInfo.method(name, parameters); } } + + interface MessageMethod { + + List parameterTypes(); + + MethodDescriptor descriptor(); + + default boolean isMessageBundleInterfaceMethod() { + return true; + } + + } + + static class SimpleMessageMethod implements MessageMethod { + + final MethodInfo method; + + SimpleMessageMethod(MethodInfo method) { + this.method = method; + } + + @Override + public List parameterTypes() { + return method.parameterTypes(); + } + + @Override + public MethodDescriptor descriptor() { + return MethodDescriptor.of(method); + } + + } + + static class EnumConstantMessageMethod implements MessageMethod { + + final MethodDescriptor descriptor; + + EnumConstantMessageMethod(MethodDescriptor descriptor) { + this.descriptor = descriptor; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public MethodDescriptor descriptor() { + return descriptor; + } + + @Override + public boolean isMessageBundleInterfaceMethod() { + return false; + } + + } } 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 5a57f0d95f270..7353f30506eaa 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 @@ -714,10 +714,12 @@ public void beforeParsing(ParserHelper parserHelper) { MessageBundleMethodBuildItem messageBundleMethod = messageBundleMethodsMap.get(templateId); if (messageBundleMethod != null) { MethodInfo method = messageBundleMethod.getMethod(); - for (ListIterator it = method.parameterTypes().listIterator(); it.hasNext();) { - Type paramType = it.next(); - String name = MessageBundleProcessor.getParameterName(method, it.previousIndex()); - parserHelper.addParameter(name, getCheckedTemplateParameterTypeName(paramType)); + if (method != null) { + for (ListIterator it = method.parameterTypes().listIterator(); it.hasNext();) { + Type paramType = it.next(); + String name = MessageBundleProcessor.getParameterName(method, it.previousIndex()); + parserHelper.addParameter(name, getCheckedTemplateParameterTypeName(paramType)); + } } } } @@ -759,9 +761,7 @@ public void beforeParsing(ParserHelper parserHelper) { for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) { Template template = dummyEngine.parse(messageBundleMethod.getTemplate(), null, messageBundleMethod.getTemplateId()); analysis.add(new TemplateAnalysis(messageBundleMethod.getTemplateId(), template.getGeneratedId(), - template.getExpressions(), template.getParameterDeclarations(), - messageBundleMethod.getMethod().declaringClass().name() + "#" + messageBundleMethod.getMethod().name() - + "()", + template.getExpressions(), template.getParameterDeclarations(), messageBundleMethod.getPathForAnalysis(), template.getFragmentIds())); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java new file mode 100644 index 0000000000000..8ac3a9e739810 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java @@ -0,0 +1,74 @@ +package io.quarkus.qute.deployment.i18n; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Template; +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; +import io.quarkus.test.QuarkusUnitTest; + +public class MessageBundleEnumTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Messages.class, MyEnum.class) + .addAsResource("messages/enu.properties") + .addAsResource("messages/enu_cs.properties") + .addAsResource(new StringAsset( + "{enu:myEnum(MyEnum:ON)}::{enu:myEnum(MyEnum:OFF)}::{enu:myEnum(MyEnum:UNDEFINED)}::" + + "{enu:shortEnum(MyEnum:ON)}::{enu:shortEnum(MyEnum:OFF)}::{enu:shortEnum(MyEnum:UNDEFINED)}::" + + "{enu:foo(MyEnum:ON)}::{enu:foo(MyEnum:OFF)}::{enu:foo(MyEnum:UNDEFINED)}::" + + "{enu:locFileOverride(MyEnum:ON)}::{enu:locFileOverride(MyEnum:OFF)}::{enu:locFileOverride(MyEnum:UNDEFINED)}"), + "templates/foo.html")); + + @Inject + Template foo; + + @Test + public void testMessages() { + assertEquals("On::Off::Undefined::1::0::U::+::-::_::on::off::undefined", foo.render()); + assertEquals("Zapnuto::Vypnuto::Nedefinováno::1::0::N::+::-::_::zap::vyp::nedef", + foo.instance().setLocale("cs").render()); + } + + @MessageBundle(value = "enu", locale = "en") + public interface Messages { + + // Replaced with: + // @Message("{#when myEnum}" + // + "{#is ON}{enu:myEnum_ON}" + // + "{#is OFF}{enu:myEnum_OFF}" + // + "{#is UNDEFINED}{enu:myEnum_UNDEFINED}" + // + "{/when}") + @Message + String myEnum(MyEnum myEnum); + + // Replaced with: + // @Message("{#when myEnum}" + // + "{#is ON}{enu:shortEnum_ON}" + // + "{#is OFF}{enu:shortEnum_OFF}" + // + "{#is UNDEFINED}{enu:shortEnum_UNDEFINED}" + // + "{/when}") + @Message + String shortEnum(MyEnum myEnum); + + @Message("{#when myEnum}" + + "{#is ON}+" + + "{#is OFF}-" + + "{#else}_" + + "{/when}") + String foo(MyEnum myEnum); + + @Message + String locFileOverride(MyEnum myEnum); + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java index 89c944458e999..fcc4f14a9c414 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java @@ -10,7 +10,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.qute.Template; -import io.quarkus.qute.TemplateEnum; import io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; import io.quarkus.test.QuarkusUnitTest; @@ -20,7 +19,7 @@ public class MessageBundleLogicalLineTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addClasses(Messages.class) + .addClasses(Messages.class, MyEnum.class) .addAsResource("messages/msg_cs.properties") .addAsResource(new StringAsset( "{msg:hello('Edgar')}::{msg:helloNextLine('Edgar')}::{msg:fruits}::{msg:myEnum(MyEnum:OFF)}"), @@ -58,11 +57,4 @@ public interface Messages { } - @TemplateEnum - public enum MyEnum { - ON, - OFF, - UNDEFINED - } - } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java index 6acf6738cb8ed..c9349a722dd84 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java @@ -83,7 +83,7 @@ public void testResolvers() { foo.instance().render()); assertEquals("Hello world! Ahoj Jachym! Hello you guys! Hello alpha! Hello! Hello foo from alpha!", foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render()); - assertEquals("Hallo Welt! Hallo Jachym! Hello you guys! Hello alpha! Hello! Hello foo from alpha!", + assertEquals("Hallo Welt! Hallo Jachym! Hallo you guys! Hello alpha! Hello! Hello foo from alpha!", foo.instance().setLocale(Locale.GERMAN).render()); assertEquals("Dot test!", engine.parse("{msg:['dot.test']}").render()); assertEquals("Hello world! Hello Malachi Constant!", diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java new file mode 100644 index 0000000000000..7e26e81d95345 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java @@ -0,0 +1,10 @@ +package io.quarkus.qute.deployment.i18n; + +import io.quarkus.qute.TemplateEnum; + +@TemplateEnum +public enum MyEnum { + ON, + OFF, + UNDEFINED +} \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/enu.properties b/extensions/qute/deployment/src/test/resources/messages/enu.properties new file mode 100644 index 0000000000000..072f933eb0881 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/enu.properties @@ -0,0 +1,13 @@ +myEnum_ON=On +myEnum_OFF=Off +myEnum_UNDEFINED=Undefined + +shortEnum_ON=1 +shortEnum_OFF=0 +shortEnum_UNDEFINED=U + +locFileOverride={#when myEnum}\ + {#is ON}on\ + {#is OFF}off\ + {#else}undefined\ + {/when} \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties b/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties new file mode 100644 index 0000000000000..e3f5c0a2ae6de --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties @@ -0,0 +1,13 @@ +myEnum_ON=Zapnuto +myEnum_OFF=Vypnuto +myEnum_UNDEFINED=Nedefinováno + +shortEnum_ON=1 +shortEnum_OFF=0 +shortEnum_UNDEFINED=N + +locFileOverride={#when myEnum}\ + {#is ON}zap\ + {#is OFF}vyp\ + {#else}nedef\ + {/when} \ No newline at end of file diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java index 8f4c68664af85..93c5fbe6b1327 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java @@ -14,7 +14,8 @@ * {@link MessageBundle#defaultKey()}. *

* The {@link #value()} defines the template of a message. The method parameters can be used in this template. All the message - * templates are validated at build time. + * templates are validated at build time. If there is no template defined the template from a localized file is taken. In case + * the value is not provided at all the build fails. *

* Note that any method declared on a message bundle interface is consireded a message bundle method. If not annotated with this * annotation then the defaulted values are used for the key and template. @@ -22,6 +23,30 @@ * All message bundle methods must return {@link String}. If a message bundle method does not return string then the build * fails. * + *

Enums

+ * There is a convenient way to localize enums. + *

+ * If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined then + * it + * receives a generated template: + * + *

+ * {#when enumParamName}
+ *     {#is CONSTANT1}{msg:methodName_CONSTANT1}
+ *     {#is CONSTANT2}{msg:methodName_CONSTANT2}
+ * {/when}
+ * 
+ * + * Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and + * values for all constant message keys: + * + *
+ * methodName_CONSTANT1=Value 1
+ * methodName_CONSTANT2=Value 2
+ * 
+ * + * In a template, an enum constant can be localized with a message bundle method {@code msg:methodName(enumConstant)}. + * * @see MessageBundle */ @Retention(RUNTIME) @@ -69,6 +94,8 @@ * This value has higher priority over a message template specified in a localized file, and it's * considered a good practice to specify it. In case the value is not provided and there is no * match in the localized file too, the build fails. + *

+ * There is a convenient way to localize enums. See the javadoc of {@link Message}. * * @return the message template */ From e59693a5bebb1cb20af4eb7e5f7d17e453f9afd4 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 18 Apr 2024 16:33:33 +0200 Subject: [PATCH 72/72] Fix license URL in JReleaser config --- devtools/cli/distribution/jreleaser-maintenance.yml | 2 +- devtools/cli/distribution/jreleaser-preview.yml | 2 +- devtools/cli/distribution/jreleaser.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/devtools/cli/distribution/jreleaser-maintenance.yml b/devtools/cli/distribution/jreleaser-maintenance.yml index 15772e91a8ba7..5847f0b4229eb 100644 --- a/devtools/cli/distribution/jreleaser-maintenance.yml +++ b/devtools/cli/distribution/jreleaser-maintenance.yml @@ -18,7 +18,7 @@ project: - java links: homepage: https://quarkus.io - license: https://github.com/quarkusio/quarkus/blob/main/LICENSE.txt + license: https://github.com/quarkusio/quarkus/blob/main/LICENSE release: github: diff --git a/devtools/cli/distribution/jreleaser-preview.yml b/devtools/cli/distribution/jreleaser-preview.yml index cbfeab135468c..1de4d9a832d17 100644 --- a/devtools/cli/distribution/jreleaser-preview.yml +++ b/devtools/cli/distribution/jreleaser-preview.yml @@ -18,7 +18,7 @@ project: - java links: homepage: https://quarkus.io - license: https://github.com/quarkusio/quarkus/blob/main/LICENSE.txt + license: https://github.com/quarkusio/quarkus/blob/main/LICENSE release: github: diff --git a/devtools/cli/distribution/jreleaser.yml b/devtools/cli/distribution/jreleaser.yml index d8be28fdd50d2..e4e37b0216b86 100644 --- a/devtools/cli/distribution/jreleaser.yml +++ b/devtools/cli/distribution/jreleaser.yml @@ -18,7 +18,7 @@ project: - java links: homepage: https://quarkus.io - license: https://github.com/quarkusio/quarkus/blob/main/LICENSE.txt + license: https://github.com/quarkusio/quarkus/blob/main/LICENSE release: github: