Skip to content

Commit 7d8144f

Browse files
mabartosshawkins
andauthored
Wildcard mappers should be implicitly handled and value propagated (keycloak#41026)
* Wildcard mappers should be implicitly handled and value propagated Closes keycloak#40977 Co-authored-by: Steven Hawkins <[email protected]> Signed-off-by: Martin Bartoš <[email protected]> * Include additional mapping only when from() is used Signed-off-by: Martin Bartoš <[email protected]> --------- Signed-off-by: Martin Bartoš <[email protected]> Co-authored-by: Steven Hawkins <[email protected]>
1 parent 3c30c46 commit 7d8144f

File tree

14 files changed

+368
-200
lines changed

14 files changed

+368
-200
lines changed

quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java

Lines changed: 102 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -112,99 +112,104 @@ public class DatabaseOptions {
112112
.hidden()
113113
.build();
114114

115-
/**
116-
* Options that have their sibling for a named datasource
117-
* Example: for `db-dialect`, `db-dialect-<datasource>` is created
118-
*/
119-
public static final List<Option<?>> OPTIONS_DATASOURCES = List.of(
120-
DB_DIALECT,
121-
DB_DRIVER,
122-
DB,
123-
DB_URL,
124-
DB_URL_HOST,
125-
DB_URL_DATABASE,
126-
DB_URL_PORT,
127-
DB_URL_PROPERTIES,
128-
DB_USERNAME,
129-
DB_PASSWORD,
130-
DB_SCHEMA,
131-
DB_POOL_INITIAL_SIZE,
132-
DB_POOL_MIN_SIZE,
133-
DB_POOL_MAX_SIZE,
134-
DB_SQL_JPA_DEBUG,
135-
DB_SQL_LOG_SLOW_QUERIES
136-
);
137-
138-
/**
139-
* In order to avoid ambiguity, we need to have unique option names for wildcard options.
140-
* This map controls overriding option name to be unique for wildcard option.
141-
*/
142-
private static final Map<String, String> DATASOURCES_OVERRIDES_SUFFIX = Map.of(
143-
DatabaseOptions.DB.getKey(), "-kind", // db-kind
144-
DatabaseOptions.DB_URL.getKey(), "-full" // db-url-full
145-
);
146-
147-
/**
148-
* You can override some {@link OptionBuilder} methods for additional datasources in this map
149-
*/
150-
private static final Map<Option<?>, Consumer<OptionBuilder<?>>> DATASOURCES_OVERRIDES_OPTIONS = Map.of(
151-
DatabaseOptions.DB, builder -> builder.defaultValue(Optional.empty()) // no default value for DB kind for datasources
152-
);
153-
154-
private static final Map<String, Option<?>> cachedDatasourceOptions = new HashMap<>();
155-
115+
public static final class Datasources {
116+
/**
117+
* Options that have their sibling for a named datasource
118+
* Example: for `db-dialect`, `db-dialect-<datasource>` is created
119+
*/
120+
public static final List<Option<?>> OPTIONS_DATASOURCES = List.of(
121+
DB_DIALECT,
122+
DB_DRIVER,
123+
DB,
124+
DB_URL,
125+
DB_URL_HOST,
126+
DB_URL_DATABASE,
127+
DB_URL_PORT,
128+
DB_URL_PROPERTIES,
129+
DB_USERNAME,
130+
DB_PASSWORD,
131+
DB_SCHEMA,
132+
DB_POOL_INITIAL_SIZE,
133+
DB_POOL_MIN_SIZE,
134+
DB_POOL_MAX_SIZE,
135+
DB_SQL_JPA_DEBUG,
136+
DB_SQL_LOG_SLOW_QUERIES
137+
);
138+
139+
/**
140+
* In order to avoid ambiguity, we need to have unique option names for wildcard options.
141+
* This map controls overriding option name to be unique for wildcard option.
142+
*/
143+
private static final Map<String, String> DATASOURCES_OVERRIDES_SUFFIX = Map.of(
144+
DatabaseOptions.DB.getKey(), "-kind", // db-kind
145+
DatabaseOptions.DB_URL.getKey(), "-full" // db-url-full
146+
);
147+
148+
/**
149+
* You can override some {@link OptionBuilder} methods for additional datasources in this map
150+
*/
151+
private static final Map<Option<?>, Consumer<OptionBuilder<?>>> DATASOURCES_OVERRIDES_OPTIONS = Map.of(
152+
DatabaseOptions.DB, builder -> builder
153+
.defaultValue(Optional.empty()) // no default value for DB kind for datasources
154+
.connectedOptions(
155+
getDatasourceOption(DatabaseOptions.DB_URL).orElseThrow(),
156+
TransactionOptions.TRANSACTION_XA_ENABLED_DATASOURCE
157+
)
158+
);
159+
160+
private static final Map<String, Option<?>> cachedDatasourceOptions = new HashMap<>();
161+
162+
/**
163+
* Get datasource option containing named datasource mapped to parent DB options.
164+
* <p>
165+
* We map DB options to named datasource options like:
166+
* <ul>
167+
* <li>{@code db-url-host --> db-url-host-<datasource>}</li>
168+
* <li>{@code db-username --> db-username-<datasource>}</li>
169+
* <li>{@code db --> db-kind-<datasource>}</li>
170+
* </ul>
171+
*/
172+
@SuppressWarnings("unchecked")
173+
public static <T> Optional<Option<T>> getDatasourceOption(Option<T> parentOption) {
174+
if (!OPTIONS_DATASOURCES.contains(parentOption)) {
175+
return Optional.empty();
176+
}
156177

157-
/**
158-
* Get datasource option containing named datasource mapped to parent DB options.
159-
* <p>
160-
* We map DB options to named datasource options like:
161-
* <ul>
162-
* <li>{@code db-url-host --> db-url-host-<datasource>}</li>
163-
* <li>{@code db-username --> db-username-<datasource>}</li>
164-
* <li>{@code db --> db-kind-<datasource>}</li>
165-
* </ul>
166-
*/
167-
@SuppressWarnings("unchecked")
168-
public static <T> Optional<Option<T>> getDatasourceOption(Option<T> parentOption) {
169-
if (!OPTIONS_DATASOURCES.contains(parentOption)) {
170-
return Optional.empty();
171-
}
178+
var key = getKeyForDatasource(parentOption);
179+
if (key.isEmpty()) {
180+
return Optional.empty();
181+
}
172182

173-
var key = getKeyForDatasource(parentOption);
174-
if (key.isEmpty()) {
175-
return Optional.empty();
176-
}
183+
// check if we already created the same option and return it from the cache
184+
Option<?> option = cachedDatasourceOptions.get(key.get());
177185

178-
// check if we already created the same option and return it from the cache
179-
Option<?> option = cachedDatasourceOptions.get(key.get());
186+
if (option == null) {
187+
var builder = parentOption.toBuilder()
188+
.key(key.get())
189+
.category(OptionCategory.DATABASE_DATASOURCES);
180190

181-
if (option == null) {
182-
var builder = parentOption.toBuilder()
183-
.key(key.get())
184-
.category(OptionCategory.DATABASE_DATASOURCES);
191+
if (!parentOption.isHidden()) {
192+
builder.description("Used for named <datasource>. " + parentOption.getDescription());
193+
}
185194

186-
if (!parentOption.isHidden()) {
187-
builder.description("Used for named <datasource>. " + parentOption.getDescription());
188-
}
195+
// override some settings for options
196+
var override = DATASOURCES_OVERRIDES_OPTIONS.get(parentOption);
197+
if (override != null) {
198+
override.accept(builder);
199+
}
189200

190-
// override some settings for options
191-
var override = DATASOURCES_OVERRIDES_OPTIONS.get(parentOption);
192-
if (override != null) {
193-
override.accept(builder);
201+
option = builder.build();
202+
cachedDatasourceOptions.put(key.get(), option);
194203
}
195-
196-
option = builder.build();
197-
cachedDatasourceOptions.put(key.get(), option);
204+
return Optional.of((Option<T>) option);
198205
}
199-
return Optional.of((Option<T>) option);
200-
}
201206

202-
/**
203-
* Get mapped datasource key based on DB option {@param option}
204-
*/
205-
public static Optional<String> getKeyForDatasource(Option<?> option) {
206-
return getKeyForDatasource(option.getKey());
207-
}
207+
/**
208+
* Get mapped datasource key based on DB option {@param option}
209+
*/
210+
public static Optional<String> getKeyForDatasource(Option<?> option) {
211+
return getKeyForDatasource(option.getKey());
212+
}
208213

209214
/**
210215
* Get mapped datasource key based on DB option {@param option}
@@ -216,17 +221,18 @@ public static Optional<String> getKeyForDatasource(String option) {
216221
.map(key -> key.concat("-<datasource>"));
217222
}
218223

219-
/**
220-
* Returns datasource option based on DB option {@code option} with actual wildcard value.
221-
* It replaces the {@code <datasource>} with actual value in {@code namedProperty}.
222-
* <p>
223-
* f.e. Consider {@code option}={@link DatabaseOptions#DB_DRIVER}, and {@code namedProperty}=my-store.
224-
* <p>
225-
* Result: {@code db-driver-my-store}
226-
*/
227-
public static Optional<String> getNamedKey(Option<?> option, String namedProperty) {
228-
return getKeyForDatasource(option)
229-
.map(key -> key.substring(0, key.indexOf("<")))
230-
.map(key -> key.concat(namedProperty));
224+
/**
225+
* Returns datasource option based on DB option {@code option} with actual wildcard value.
226+
* It replaces the {@code <datasource>} with actual value in {@code namedProperty}.
227+
* <p>
228+
* f.e. Consider {@code option}={@link DatabaseOptions#DB_DRIVER}, and {@code namedProperty}=my-store.
229+
* <p>
230+
* Result: {@code db-driver-my-store}
231+
*/
232+
public static Optional<String> getNamedKey(Option<?> option, String namedProperty) {
233+
return getKeyForDatasource(option)
234+
.map(key -> key.substring(0, key.indexOf("<")))
235+
.map(key -> key.concat(namedProperty));
236+
}
231237
}
232238
}

quarkus/config-api/src/main/java/org/keycloak/config/Option.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java.util.List;
66
import java.util.Optional;
7+
import java.util.Set;
78
import java.util.stream.Collectors;
89

910
public class Option<T> {
@@ -18,8 +19,11 @@ public class Option<T> {
1819
private final boolean strictExpectedValues;
1920
private final boolean caseInsensitiveExpectedValues;
2021
private final DeprecatedMetadata deprecatedMetadata;
22+
private final Set<String> connectedOptions;
2123

22-
public Option(Class<T> type, String key, OptionCategory category, boolean hidden, boolean buildTime, String description, Optional<T> defaultValue, List<String> expectedValues, boolean strictExpectedValues, boolean caseInsensitiveExpectedValues, DeprecatedMetadata deprecatedMetadata) {
24+
public Option(Class<T> type, String key, OptionCategory category, boolean hidden, boolean buildTime, String description,
25+
Optional<T> defaultValue, List<String> expectedValues, boolean strictExpectedValues, boolean caseInsensitiveExpectedValues,
26+
DeprecatedMetadata deprecatedMetadata, Set<String> connectedOptions) {
2327
this.type = type;
2428
this.key = key;
2529
this.category = category;
@@ -31,6 +35,7 @@ public Option(Class<T> type, String key, OptionCategory category, boolean hidden
3135
this.strictExpectedValues = strictExpectedValues;
3236
this.caseInsensitiveExpectedValues = caseInsensitiveExpectedValues;
3337
this.deprecatedMetadata = deprecatedMetadata;
38+
this.connectedOptions = connectedOptions;
3439
}
3540

3641
public Class<T> getType() {
@@ -87,6 +92,14 @@ public Option<T> withRuntimeSpecificDefault(T defaultValue) {
8792
return toBuilder().defaultValue(defaultValue).build();
8893
}
8994

95+
/**
96+
* Get connected options that have a certain relationship with the current option.
97+
* Usually when the current option is set, the connected options should be set as well.
98+
*/
99+
public Set<String> getConnectedOptions() {
100+
return connectedOptions;
101+
}
102+
90103
public OptionBuilder<T> toBuilder() {
91104
var builder = new OptionBuilder<>(key, type)
92105
.category(category)

quarkus/config-api/src/main/java/org/keycloak/config/OptionBuilder.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
import io.smallrye.common.constraint.Assert;
44

5+
import java.util.Arrays;
56
import java.util.Collection;
7+
import java.util.HashSet;
68
import java.util.List;
79
import java.util.Map;
810
import java.util.Optional;
11+
import java.util.Set;
912
import java.util.stream.Collectors;
1013
import java.util.stream.Stream;
1114

@@ -16,6 +19,8 @@ public class OptionBuilder<T> {
1619

1720
private final Class<T> type;
1821
private final Class<?> auxiliaryType;
22+
private final Set<String> connectedOptions = new HashSet<>();
23+
1924
private String key;
2025
private OptionCategory category;
2126
private boolean hidden;
@@ -134,6 +139,14 @@ public OptionBuilder<T> deprecatedValues(String note, T... values) {
134139
return this;
135140
}
136141

142+
/**
143+
* For more details, see the {@link Option#getConnectedOptions()}
144+
*/
145+
public OptionBuilder<T> connectedOptions(Option<?>... connectedOptions) {
146+
this.connectedOptions.addAll(Arrays.stream(connectedOptions).map(Option::getKey).collect(Collectors.toSet()));
147+
return this;
148+
}
149+
137150
public Option<T> build() {
138151
if (deprecatedMetadata == null && category.getSupportLevel() == ConfigSupportLevel.DEPRECATED) {
139152
deprecated();
@@ -169,7 +182,7 @@ public Option<T> build() {
169182
}
170183
}
171184

172-
return new Option<T>(type, key, category, hidden, build, description, defaultValue, expectedValues, strictExpectedValues, caseInsensitiveExpectedValues, deprecatedMetadata);
185+
return new Option<T>(type, key, category, hidden, build, description, defaultValue, expectedValues, strictExpectedValues, caseInsensitiveExpectedValues, deprecatedMetadata, connectedOptions);
173186
}
174187

175188
}

quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ private static String getProperty(Option<?> option, String namedProperty) {
282282

283283
private static String getProperty(Option<?> option, String namedProperty, String defaultValue) {
284284
return "${kc.%s:%s}".formatted(StringUtil.isNullOrEmpty(namedProperty) ? option.getKey() :
285-
DatabaseOptions.getNamedKey(option, namedProperty).orElseThrow(() -> new IllegalArgumentException("Cannot find the named property")),
285+
DatabaseOptions.Datasources.getNamedKey(option, namedProperty).orElseThrow(() -> new IllegalArgumentException("Cannot find the named property")),
286286
defaultValue);
287287
}
288288

quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@
107107
import org.keycloak.quarkus.runtime.Environment;
108108
import org.keycloak.quarkus.runtime.KeycloakRecorder;
109109
import org.keycloak.quarkus.runtime.cli.Picocli;
110-
import org.keycloak.quarkus.runtime.cli.PropertyException;
111110
import org.keycloak.quarkus.runtime.configuration.Configuration;
112111
import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider;
113112
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
@@ -382,7 +381,7 @@ void checkPersistenceUnits(List<PersistenceXmlDescriptorBuildItem> descriptors)
382381
.filter(descriptor -> !descriptor.getName().equals(DEFAULT_PERSISTENCE_UNIT)) // not default persistence unit
383382
.map(KeycloakProcessor::getDatasourceNameFromPersistenceXml)
384383
.filter(this::missingDbKind)
385-
.map(datasourceName -> DatabaseOptions.getNamedKey(DatabaseOptions.DB, datasourceName).orElseThrow()).toList();
384+
.map(datasourceName -> DatabaseOptions.Datasources.getNamedKey(DatabaseOptions.DB, datasourceName).orElseThrow()).toList();
386385

387386
if (!notSetPersistenceUnitsDBKinds.isEmpty()) {
388387
throwConfigError("Detected additional named datasources without a DB kind set, please specify: %s".formatted(String.join(",", notSetPersistenceUnitsDBKinds)));
@@ -400,7 +399,7 @@ void checkPersistenceUnits(List<PersistenceXmlDescriptorBuildItem> descriptors)
400399
* </ol>
401400
*/
402401
private boolean missingDbKind(String datasourceName) {
403-
String key = NS_KEYCLOAK_PREFIX.concat(DatabaseOptions.getNamedKey(DatabaseOptions.DB, datasourceName).orElseThrow());
402+
String key = NS_KEYCLOAK_PREFIX.concat(DatabaseOptions.Datasources.getNamedKey(DatabaseOptions.DB, datasourceName).orElseThrow());
404403
PropertyMappingInterceptor.disable();
405404
try {
406405
var from = Configuration.getConfigValue(key);
@@ -542,18 +541,30 @@ static void configurePersistenceUnitProperties(String datasourceName, ParsedPers
542541
.formatted(PersistenceUnitTransactionType.JTA.name()));
543542
}
544543

544+
// db-dialect
545+
DatabaseOptions.Datasources.getNamedKey(DatabaseOptions.DB_DIALECT, datasourceName)
546+
.flatMap(Configuration::getOptionalKcValue)
547+
.ifPresent(dialect -> unitProperties.setProperty(AvailableSettings.DIALECT, dialect));
548+
549+
// db-schema
550+
DatabaseOptions.Datasources.getNamedKey(DatabaseOptions.DB_SCHEMA, datasourceName)
551+
.flatMap(Configuration::getOptionalKcValue)
552+
.ifPresent(schema -> unitProperties.setProperty(AvailableSettings.DEFAULT_SCHEMA, schema));
553+
545554
unitProperties.setProperty(AvailableSettings.JAKARTA_TRANSACTION_TYPE, PersistenceUnitTransactionType.JTA.name());
546555
descriptor.setTransactionType(PersistenceUnitTransactionType.JTA);
547556

548557
// set datasource name
549558
unitProperties.setProperty(JdbcSettings.JAKARTA_JTA_DATASOURCE,datasourceName);
550559
unitProperties.setProperty(AvailableSettings.DATASOURCE, datasourceName); // for backward compatibility
551560

552-
DatabaseOptions.getNamedKey(DatabaseOptions.DB_SQL_JPA_DEBUG, datasourceName)
561+
// db-debug-jpql
562+
DatabaseOptions.Datasources.getNamedKey(DatabaseOptions.DB_SQL_JPA_DEBUG, datasourceName)
553563
.filter(Configuration::isKcPropertyTrue)
554564
.ifPresent(f -> unitProperties.put(AvailableSettings.USE_SQL_COMMENTS, "true"));
555565

556-
DatabaseOptions.getNamedKey(DatabaseOptions.DB_SQL_LOG_SLOW_QUERIES, datasourceName)
566+
// db-log-slow-queries-threshold
567+
DatabaseOptions.Datasources.getNamedKey(DatabaseOptions.DB_SQL_LOG_SLOW_QUERIES, datasourceName)
557568
.flatMap(Configuration::getOptionalKcValue)
558569
.ifPresent(threshold -> unitProperties.put(AvailableSettings.LOG_SLOW_QUERY, threshold));
559570
}

0 commit comments

Comments
 (0)