From 3a7f5171abf8d10e8b2f078a509a99105ff75802 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Tue, 9 Nov 2021 13:52:40 -0300 Subject: [PATCH] [KEYCLOAK-19310] - MSSQL Support --- quarkus/deployment/pom.xml | 4 + .../deployment/LiquibaseProcessor.java | 12 +- quarkus/runtime/pom.xml | 4 + .../quarkus/runtime/KeycloakRecorder.java | 2 - .../quarkus/runtime/cli/OptionRenderer.java | 13 +- .../keycloak/quarkus/runtime/cli/Picocli.java | 9 +- .../cli/PropertyMapperParameterConsumer.java | 99 ++++++++++++ .../quarkus/runtime/cli/command/Start.java | 2 - .../configuration/ConfigArgsConfigSource.java | 1 - .../mappers/ClusteringPropertyMappers.java | 1 - .../mappers/DatabasePropertyMappers.java | 25 +-- .../QuarkusKeycloakSessionFactory.java | 2 - .../integration/web/QuarkusRequestFilter.java | 2 +- .../runtime/storage/database/Database.java | 149 ++++++++++++------ .../QuarkusJpaConnectionProviderFactory.java | 2 - .../liquibase/FastServiceLocator.java | 9 ++ .../database/liquibase/KeycloakLogger.java | 2 +- .../QuarkusLiquibaseConnectionProvider.java | 8 - .../database/CustomMSSQLDatabase.java | 35 ++++ .../storage/infinispan/CacheInitializer.java | 2 - .../provider/quarkus/ConfigurationTest.java | 2 +- 21 files changed, 292 insertions(+), 93 deletions(-) create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/PropertyMapperParameterConsumer.java create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/database/CustomMSSQLDatabase.java diff --git a/quarkus/deployment/pom.xml b/quarkus/deployment/pom.xml index 15ce8c862c8..cc3e2550b3b 100644 --- a/quarkus/deployment/pom.xml +++ b/quarkus/deployment/pom.xml @@ -58,6 +58,10 @@ io.quarkus quarkus-jdbc-mysql-deployment + + io.quarkus + quarkus-jdbc-mssql-deployment + io.quarkus quarkus-bootstrap-core diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/LiquibaseProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/LiquibaseProcessor.java index c7a82d0bdf2..09208184107 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/LiquibaseProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/LiquibaseProcessor.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; @@ -35,11 +36,12 @@ class LiquibaseProcessor { @Record(ExecutionTime.STATIC_INIT) @BuildStep - void configure(KeycloakRecorder recorder, CombinedIndexBuildItem indexBuildItem) { + void configure(KeycloakRecorder recorder, List jdbcDataSources, CombinedIndexBuildItem indexBuildItem) { DotName liquibaseServiceName = DotName.createSimple(LiquibaseService.class.getName()); Map> services = new HashMap<>(); - IndexView index = indexBuildItem.getIndex(); + JdbcDataSourceBuildItem dataSourceBuildItem = jdbcDataSources.get(0); + String dbKind = dataSourceBuildItem.getDbKind(); for (Class c : Arrays.asList(liquibase.diff.compare.DatabaseObjectComparator.class, liquibase.parser.NamespaceDetails.class, @@ -61,7 +63,7 @@ class LiquibaseProcessor { } else { classes.addAll(index.getAllKnownSubclasses(DotName.createSimple(c.getName()))); } - filterImplementations(c, classes); + filterImplementations(c, dbKind, classes); for (ClassInfo found : classes) { if (Modifier.isAbstract(found.flags()) || Modifier.isInterface(found.flags()) || @@ -85,10 +87,10 @@ class LiquibaseProcessor { recorder.configureLiquibase(services); } - private void filterImplementations(Class types, Set classes) { + private void filterImplementations(Class types, String dbKind, Set classes) { if (Database.class.equals(types)) { // removes unsupported databases - classes.removeIf(classInfo -> !org.keycloak.quarkus.runtime.storage.database.Database.isSupported(classInfo.name().toString())); + classes.removeIf(classInfo -> !org.keycloak.quarkus.runtime.storage.database.Database.isLiquibaseDatabaseSupported(classInfo.name().toString(), dbKind)); } } } diff --git a/quarkus/runtime/pom.xml b/quarkus/runtime/pom.xml index 0d666eb81ac..19999a2d656 100644 --- a/quarkus/runtime/pom.xml +++ b/quarkus/runtime/pom.xml @@ -59,6 +59,10 @@ io.quarkus quarkus-jdbc-mysql + + io.quarkus + quarkus-jdbc-mssql + io.quarkus quarkus-core diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java index 54bb70dbf39..655b3ac276a 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java @@ -42,8 +42,6 @@ import liquibase.servicelocator.ServiceLocator; @Recorder public class KeycloakRecorder { - private static final Logger LOGGER = Logger.getLogger(KeycloakRecorder.class); - public void configureLiquibase(Map> services) { LogFactory.setInstance(new LogFactory() { final KeycloakLogger logger = new KeycloakLogger(); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/OptionRenderer.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/OptionRenderer.java index bbe70996e25..d483214c6c3 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/OptionRenderer.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/OptionRenderer.java @@ -52,7 +52,7 @@ public class OptionRenderer implements CommandLine.Help.IOptionRenderer { throw new CommandLine.PicocliException("Option[" + option + "] description should have a single line."); } - Text description = scheme.text(descriptions[0]); + Text description = formatDescription(descriptions, option, scheme); if (EMPTY_TEXT.equals(shortName)) { result[0] = new Text[] { longName, description }; @@ -63,6 +63,17 @@ public class OptionRenderer implements CommandLine.Help.IOptionRenderer { return result; } + private Text formatDescription(String[] descriptions, OptionSpec option, ColorScheme scheme) { + String description = descriptions[0]; + String defaultValue = option.defaultValue(); + + if (defaultValue != null) { + description = description + " Default: " + defaultValue + "."; + } + + return scheme.text(description); + } + private Text createLongName(OptionSpec option, ColorScheme scheme) { Text name = scheme.optionText(option.longestName()); String paramLabel = formatParamLabel(option); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java index 0471e59047e..37ccdd3dded 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java @@ -67,6 +67,7 @@ public final class Picocli { private static final String ARG_SEPARATOR = ";;"; public static final String ARG_PREFIX = "--"; + public static final String ARG_SHORT_PREFIX = "-"; public static final String ARG_PART_SEPARATOR = "-"; public static final char ARG_KEY_VALUE_SEPARATOR = '='; public static final Pattern ARG_SPLIT = Pattern.compile(";;"); @@ -342,6 +343,7 @@ public final class Picocli { .description("Enables a group of features. Possible values are: " + String.join(",", featuresExpectedValues)) .paramLabel("feature") .completionCandidates(featuresExpectedValues) + .parameterConsumer(PropertyMapperParameterConsumer.INSTANCE) .type(String.class) .build()); @@ -352,6 +354,7 @@ public final class Picocli { .description("Enables the " + feature.name() + " feature.") .paramLabel(String.join("|", expectedValues)) .type(String.class) + .parameterConsumer(PropertyMapperParameterConsumer.INSTANCE) .completionCandidates(expectedValues) .build()); } @@ -385,12 +388,14 @@ public final class Picocli { } String defaultValue = mapper.getDefaultValue(); + Iterable expectedValues = mapper.getExpectedValues(); argGroupBuilder.addArg(OptionSpec.builder(name) .defaultValue(defaultValue) - .description(description + (defaultValue == null ? "" : " Default: ${DEFAULT-VALUE}.")) + .description(description) .paramLabel(mapper.getParamLabel()) - .completionCandidates(mapper.getExpectedValues()) + .completionCandidates(expectedValues) + .parameterConsumer(PropertyMapperParameterConsumer.INSTANCE) .type(String.class) .build()); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/PropertyMapperParameterConsumer.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/PropertyMapperParameterConsumer.java new file mode 100644 index 00000000000..df5cd81f2fb --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/PropertyMapperParameterConsumer.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.cli; + +import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX; + +import java.util.Iterator; +import java.util.Stack; + +import picocli.CommandLine; +import picocli.CommandLine.Model.ArgSpec; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Model.OptionSpec; +import picocli.CommandLine.ParameterException; + +public final class PropertyMapperParameterConsumer implements CommandLine.IParameterConsumer { + + static final CommandLine.IParameterConsumer INSTANCE = new PropertyMapperParameterConsumer(); + + private PropertyMapperParameterConsumer() { + // singleton + } + + @Override + public void consumeParameters(Stack args, ArgSpec argSpec, + CommandSpec commandSpec) { + if (argSpec instanceof OptionSpec) { + validateOption(args, argSpec, commandSpec); + } + } + + private void validateOption(Stack args, ArgSpec argSpec, CommandSpec commandSpec) { + OptionSpec option = (OptionSpec) argSpec; + String name = String.join(", ", option.names()); + CommandLine commandLine = commandSpec.commandLine(); + + if (args.isEmpty() || !isOptionValue(args.peek())) { + throw new ParameterException( + commandLine, "Missing required value for option '" + name + "' (" + argSpec.paramLabel() + ")." + getExpectedValuesMessage(argSpec, option)); + } + + // consumes the value + String value = args.pop(); + + if (!args.isEmpty() && isOptionValue(args.peek())) { + throw new ParameterException( + commandLine, "Option '" + name + "' expects a single value (" + argSpec.paramLabel() + ")" + getExpectedValuesMessage(argSpec, option)); + } + + if (isExpectedValue(option, value)) { + return; + } + + throw new ParameterException( + commandLine, "Invalid value for option '" + name + "': " + value + "." + getExpectedValuesMessage(argSpec, option)); + } + + private boolean isOptionValue(String arg) { + return !(arg.startsWith(ARG_PREFIX) || arg.startsWith(Picocli.ARG_SHORT_PREFIX)); + } + + private String getExpectedValuesMessage(ArgSpec argSpec, OptionSpec option) { + return option.completionCandidates().iterator().hasNext() ? " Expected values are: " + String.join(", ", argSpec.completionCandidates()) : ""; + } + + private boolean isExpectedValue(OptionSpec option, String value) { + Iterator expectedValues = option.completionCandidates().iterator(); + + if (!expectedValues.hasNext()) { + // accept any + return true; + } + + while (expectedValues.hasNext()) { + String expectedValue = expectedValues.next(); + + if (expectedValue.equals(value)) { + return true; + } + } + + return false; + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java index b86adda99c1..47c15701c27 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java @@ -19,8 +19,6 @@ package org.keycloak.quarkus.runtime.cli.command; import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL; -import org.keycloak.quarkus.runtime.cli.Picocli; - import picocli.CommandLine; import picocli.CommandLine.Command; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java index b8769fb9374..3e8ac580264 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/ConfigArgsConfigSource.java @@ -26,7 +26,6 @@ import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvi import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.regex.Pattern; import org.jboss.logging.Logger; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClusteringPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClusteringPropertyMappers.java index 01ce8ebcf9d..93181b9ed84 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClusteringPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ClusteringPropertyMappers.java @@ -19,7 +19,6 @@ final class ClusteringPropertyMappers { "Value 'default' enables clustering for infinispan caches.") .paramLabel("mode") .isBuildTimeProperty(true) - .expectedValues(Arrays.asList("local", "default")) .build(), builder().from("cluster-stack") .to("kc.spi.connections-infinispan.quarkus.stack") diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java index d50ed4c9879..28f66ca54cf 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java @@ -3,16 +3,15 @@ package org.keycloak.quarkus.runtime.configuration.mappers; import io.smallrye.config.ConfigSourceInterceptorContext; import org.keycloak.quarkus.runtime.storage.database.Database; -import java.util.Arrays; +import java.util.Optional; import java.util.function.BiFunction; +import static java.util.Arrays.asList; import static org.keycloak.quarkus.runtime.configuration.mappers.Messages.invalidDatabaseVendor; import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException; final class DatabasePropertyMappers { - private static final String[] supportedDatabaseVendors = {"h2-file", "h2-mem", "mariadb", "mysql", "postgres", "postgres-95", "postgres-10"}; - private DatabasePropertyMappers(){} public static PropertyMapper[] getDatabasePropertyMappers() { @@ -29,10 +28,10 @@ final class DatabasePropertyMappers { builder().from("db"). to("quarkus.datasource.db-kind") .isBuildTimeProperty(true) - .transformer(getSupportedDbValue()) - .description("The database vendor. Possible values are: " + String.join(",", supportedDatabaseVendors)) + .transformer(toDatabaseKind()) + .description("The database vendor. Possible values are: " + String.join(", ", Database.getAliases())) .paramLabel("vendor") - .expectedValues(Arrays.asList(supportedDatabaseVendors)) + .expectedValues(asList(Database.getAliases())) .build(), builder().from("db") .to("quarkus.datasource.jdbc.transactions") @@ -83,13 +82,17 @@ final class DatabasePropertyMappers { }; } - private static BiFunction getSupportedDbValue() { + private static BiFunction toDatabaseKind() { return (db, context) -> { - if (Database.isSupported(db)) { - return Database.getDatabaseKind(db).orElse(db); + Optional databaseKind = Database.getDatabaseKind(db); + + if (databaseKind.isPresent()) { + return databaseKind.get(); } - addInitializationException(invalidDatabaseVendor(db, "h2-file", "h2-mem", "mariadb", "mysql", "postgres", "postgres-95", "postgres-10")); - return "h2"; + + addInitializationException(invalidDatabaseVendor(db, Database.getAliases())); + + return null; }; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSessionFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSessionFactory.java index d2fcb761e06..1b877e0f752 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSessionFactory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSessionFactory.java @@ -32,8 +32,6 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissions; public final class QuarkusKeycloakSessionFactory extends DefaultKeycloakSessionFactory { - private static final Logger logger = Logger.getLogger(QuarkusKeycloakSessionFactory.class); - public static QuarkusKeycloakSessionFactory getInstance() { if (INSTANCE == null) { INSTANCE = new QuarkusKeycloakSessionFactory(); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/web/QuarkusRequestFilter.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/web/QuarkusRequestFilter.java index 19a2735aab9..a7a40eb0d4e 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/web/QuarkusRequestFilter.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/web/QuarkusRequestFilter.java @@ -46,7 +46,7 @@ public class QuarkusRequestFilter extends AbstractRequestFilter implements Handl context.vertx().executeBlocking(promise -> { ClientConnection connection = createClientConnection(context.request()); - filter(connection, (session) -> { + filter(connection, session -> { try { configureContextualData(context, connection, session); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/Database.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/Database.java index ddaed8db0d7..79e05ca1182 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/Database.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/Database.java @@ -17,34 +17,35 @@ package org.keycloak.quarkus.runtime.storage.database; +import static java.util.Arrays.asList; + import java.io.File; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; -import liquibase.database.core.H2Database; -import liquibase.database.core.PostgresDatabase; -import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase; -import org.keycloak.connections.jpa.updater.liquibase.UpdatedMariaDBDatabase; -import org.keycloak.connections.jpa.updater.liquibase.UpdatedMySqlDatabase; - public final class Database { - private static Map DATABASES = new HashMap<>(); + private static final Map DATABASES = new HashMap<>(); static { for (Vendor vendor : Vendor.values()) { - DATABASES.put(vendor.name().toLowerCase(), vendor); - for (String alias : vendor.aliases) { DATABASES.put(alias, vendor); } } } - public static boolean isSupported(String alias) { - return DATABASES.containsKey(alias); + public static boolean isLiquibaseDatabaseSupported(String databaseType, String dbKind) { + for (Vendor vendor : DATABASES.values()) { + if (vendor.liquibaseTypes.contains(databaseType) && vendor.isOfKind(dbKind)) { + return true; + } + } + + return false; } public static Optional getDatabaseKind(String alias) { @@ -76,74 +77,120 @@ public final class Database { return Optional.of(vendor.driver); } - + public static Optional getDialect(String alias) { Vendor vendor = DATABASES.get(alias); - + if (vendor == null) { return Optional.empty(); } - + return Optional.of(vendor.dialect.apply(alias)); } + public static String[] getAliases() { + return DATABASES.keySet().stream().sorted().toArray(String[]::new); + } + private enum Vendor { - H2("h2", "org.h2.jdbcx.JdbcDataSource", "io.quarkus.hibernate.orm.runtime.dialect.QuarkusH2Dialect", - new Function() { - @Override - public String apply(String alias) { - if ("h2-file".equalsIgnoreCase(alias)) { - return "jdbc:h2:file:${kc.home.dir:${kc.db.url.path:~}}" + File.separator + "${kc.data.dir:data}" - + File.separator + "h2" + File.separator + "keycloakdb${kc.db.url.properties:;;AUTO_SERVER=TRUE}"; + H2("h2", + "org.h2.jdbcx.JdbcDataSource", + "io.quarkus.hibernate.orm.runtime.dialect.QuarkusH2Dialect", + new Function() { + @Override + public String apply(String alias) { + if ("h2-file".equalsIgnoreCase(alias)) { + return "jdbc:h2:file:${kc.home.dir:${kc.db.url.path:~}}" + File.separator + "${kc.data.dir:data}" + + File.separator + "h2" + File.separator + + "keycloakdb${kc.db.url.properties:;;AUTO_SERVER=TRUE}"; + } + return "jdbc:h2:mem:keycloakdb${kc.db.url.properties:}"; } - return "jdbc:h2:mem:keycloakdb${kc.db.url.properties:}"; - } - }, "h2-mem", "h2-file", H2Database.class - .getName()), - MYSQL("mysql", "com.mysql.cj.jdbc.MysqlXADataSource", "org.hibernate.dialect.MySQL8Dialect", - "jdbc:mysql://${kc.db.url.host:localhost}/${kc.db.url.database:keycloak}${kc.db.url.properties:}", - UpdatedMySqlDatabase.class - .getName()), - MARIADB("mariadb", "org.mariadb.jdbc.MySQLDataSource", "org.hibernate.dialect.MariaDBDialect", - "jdbc:mariadb://${kc.db.url.host:localhost}/${kc.db.url.database:keycloak}${kc.db.url.properties:}", - UpdatedMariaDBDatabase.class - .getName()), - POSTGRES("postgresql", "org.postgresql.xa.PGXADataSource", new Function() { - @Override - public String apply(String alias) { - if ("postgres-95".equalsIgnoreCase(alias)) { - return "io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL95Dialect"; - } - return "io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect"; - } - }, "jdbc:postgresql://${kc.db.url.host:localhost}/${kc.db.url.database:keycloak}${kc.db.url.properties:}", - "postgres-95", "postgres-10", PostgresDatabase.class.getName(), PostgresPlusDatabase.class.getName()); + }, + asList("liquibase.database.core.H2Database"), + "h2-mem", "h2-file" + ), + MYSQL("mysql", + "com.mysql.cj.jdbc.MysqlXADataSource", + "org.hibernate.dialect.MySQL8Dialect", + + "jdbc:mysql://${kc.db.url.host:localhost}/${kc.db.url.database:keycloak}${kc.db.url.properties:}", + asList("org.keycloak.connections.jpa.updater.liquibase.UpdatedMySqlDatabase") + ), + MARIADB("mariadb", + "org.mariadb.jdbc.MySQLDataSource", + "org.hibernate.dialect.MariaDBDialect", + "jdbc:mariadb://${kc.db.url.host:localhost}/${kc.db.url.database:keycloak}${kc.db.url.properties:}", + asList("org.keycloak.connections.jpa.updater.liquibase.UpdatedMariaDBDatabase") + ), + POSTGRES("postgresql", + "org.postgresql.xa.PGXADataSource", + new Function() { + @Override + public String apply(String alias) { + if ("postgres-95".equalsIgnoreCase(alias)) { + return "io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL95Dialect"; + } + return "io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect"; + } + }, + "jdbc:postgresql://${kc.db.url.host:localhost}/${kc.db.url.database:keycloak}${kc.db.url.properties:}", + asList("liquibase.database.core.PostgresDatabase", + "org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase"), + "postgres", "postgres-95" + ), + MSSQL("mssql", + "com.microsoft.sqlserver.jdbc.SQLServerXADataSource", + new Function() { + @Override + public String apply(String alias) { + if ("mssql-12".equals(alias)) { + return "org.hibernate.dialect.SQLServer2012Dialect"; + } + // quarkus latest/default + return "org.hibernate.dialect.SQLServer2016Dialect"; + } + }, + "jdbc:sqlserver://${kc.db.url.host:localhost}:1433;databaseName=${kc.db.url.database:keycloak}${kc.db.url.properties:}", + asList("org.keycloak.quarkus.runtime.storage.database.liquibase.database.CustomMSSQLDatabase"), + "mssql", "mssql-2012" + ); final String databaseKind; final String driver; final Function dialect; final Function defaultUrl; + final List liquibaseTypes; final String[] aliases; - Vendor(String databaseKind, String driver, String dialect, String defaultUrl, String... aliases) { - this(databaseKind, driver, (alias) -> dialect, (alias) -> defaultUrl, aliases); + Vendor(String databaseKind, String driver, String dialect, String defaultUrl, List liquibaseTypes, + String... aliases) { + this(databaseKind, driver, alias -> dialect, alias -> defaultUrl, liquibaseTypes, aliases); } - Vendor(String databaseKind, String driver, String dialect, Function defaultUrl, String... aliases) { - this(databaseKind, driver, (alias) -> dialect, defaultUrl, aliases); + Vendor(String databaseKind, String driver, String dialect, Function defaultUrl, + List liquibaseTypes, String... aliases) { + this(databaseKind, driver, alias -> dialect, defaultUrl, liquibaseTypes, aliases); } - Vendor(String databaseKind, String driver, Function dialect, String defaultUrl, String... aliases) { - this(databaseKind, driver, dialect, (alias) -> defaultUrl, aliases); + Vendor(String databaseKind, String driver, Function dialect, String defaultUrl, + List liquibaseTypes, String... aliases) { + this(databaseKind, driver, dialect, alias -> defaultUrl, liquibaseTypes, aliases); } Vendor(String databaseKind, String driver, Function dialect, Function defaultUrl, - String... aliases) { + List liquibaseTypes, + String... aliases) { this.databaseKind = databaseKind; this.driver = driver; this.dialect = dialect; this.defaultUrl = defaultUrl; - this.aliases = aliases; + this.liquibaseTypes = liquibaseTypes; + this.aliases = aliases.length == 0 ? new String[] { databaseKind } : aliases; + } + + public boolean isOfKind(String dbKind) { + return databaseKind.equals(dbKind); } } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/jpa/QuarkusJpaConnectionProviderFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/jpa/QuarkusJpaConnectionProviderFactory.java index 4330c31f25d..fc887c16fae 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/jpa/QuarkusJpaConnectionProviderFactory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/jpa/QuarkusJpaConnectionProviderFactory.java @@ -45,7 +45,6 @@ import javax.transaction.Transaction; import com.fasterxml.jackson.core.type.TypeReference; import io.quarkus.runtime.Quarkus; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.SessionFactoryImpl; import org.jboss.logging.Logger; import org.keycloak.Config; @@ -117,7 +116,6 @@ public final class QuarkusJpaConnectionProviderFactory implements JpaConnectionP } private void addSpecificNamedQueries(KeycloakSession session, Connection connection) { - SessionFactoryImplementor sfi = emf.unwrap(SessionFactoryImplementor.class); EntityManager em = createEntityManager(session); try { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/FastServiceLocator.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/FastServiceLocator.java index 0166ded534c..cb16345fee7 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/FastServiceLocator.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/FastServiceLocator.java @@ -79,6 +79,15 @@ public class FastServiceLocator extends ServiceLocator { getPackages().remove("liquibase.parser.core.json"); getPackages().remove("liquibase.serializer.core.json"); + // register only the implementations related to the chosen db + for (String databaseImpl : services.get(Database.class.getName())) { + try { + register((Database) getClass().getClassLoader().loadClass(databaseImpl).getDeclaredConstructor().newInstance()); + } catch (Exception cause) { + throw new RuntimeException("Failed to load database implementation", cause); + } + } + this.services = services; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/KeycloakLogger.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/KeycloakLogger.java index cacec56a9f3..69d05404ac6 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/KeycloakLogger.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/KeycloakLogger.java @@ -24,7 +24,7 @@ import liquibase.logging.Logger; public class KeycloakLogger implements Logger { - private static final org.jboss.logging.Logger logger = org.jboss.logging.Logger.getLogger(QuarkusLiquibaseConnectionProvider.class); + private static final org.jboss.logging.Logger logger = org.jboss.logging.Logger.getLogger(KeycloakLogger.class); @Override public void setName(String name) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/QuarkusLiquibaseConnectionProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/QuarkusLiquibaseConnectionProvider.java index 091ab876e75..e50351888c9 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/QuarkusLiquibaseConnectionProvider.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/QuarkusLiquibaseConnectionProvider.java @@ -29,9 +29,6 @@ import org.keycloak.Config; import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.connections.jpa.JpaConnectionProviderFactory; import org.keycloak.connections.jpa.updater.liquibase.MySQL8VarcharType; -import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase; -import org.keycloak.connections.jpa.updater.liquibase.UpdatedMariaDBDatabase; -import org.keycloak.connections.jpa.updater.liquibase.UpdatedMySqlDatabase; import org.keycloak.connections.jpa.updater.liquibase.conn.CustomChangeLogHistoryService; import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider; import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory; @@ -79,11 +76,6 @@ public class QuarkusLiquibaseConnectionProvider implements LiquibaseConnectionPr JpaConnectionProviderFactory jpaConnectionProvider = (JpaConnectionProviderFactory) session .getKeycloakSessionFactory().getProviderFactory(JpaConnectionProvider.class); - // register our custom databases - locator.register(new PostgresPlusDatabase()); - locator.register(new UpdatedMySqlDatabase()); - locator.register(new UpdatedMariaDBDatabase()); - // registers only the database we are using try (Connection connection = jpaConnectionProvider.getConnection()) { Database database = DatabaseFactory.getInstance() diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/database/CustomMSSQLDatabase.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/database/CustomMSSQLDatabase.java new file mode 100644 index 00000000000..7a38fdbfc54 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/database/CustomMSSQLDatabase.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.storage.database.liquibase.database; + +import liquibase.database.core.MSSQLDatabase; + +public class CustomMSSQLDatabase extends MSSQLDatabase { + + private static String ENGINE_EDITION; + + @Override + public String getEngineEdition() { + // no need to query engine edition every time + // it should be safe to update without any synchronization code as liquibase runs from a single thread + if (ENGINE_EDITION == null) { + return ENGINE_EDITION = super.getEngineEdition(); + } + return ENGINE_EDITION; + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java index 6866f2c7e7e..e5d2b00d919 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheInitializer.java @@ -26,8 +26,6 @@ import org.keycloak.Config; public class CacheInitializer { - private static final Logger log = Logger.getLogger(CacheInitializer.class); - private final String config; public CacheInitializer(String config) { diff --git a/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java index b71e768f458..1375c30c116 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/provider/quarkus/ConfigurationTest.java @@ -235,7 +235,7 @@ public class ConfigurationTest { @Test public void testDatabaseKindProperties() { - System.setProperty(CLI_ARGS, "--db=postgres-10" + ARG_SEPARATOR + "--db-url=jdbc:postgresql://localhost/keycloak"); + System.setProperty(CLI_ARGS, "--db=postgres" + ARG_SEPARATOR + "--db-url=jdbc:postgresql://localhost/keycloak"); SmallRyeConfig config = createConfig(); assertEquals("io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect", config.getConfigValue("quarkus.hibernate-orm.dialect").getValue());