Skip to content

Commit

Permalink
Improve compatibility of the REST Client configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
radcortez committed Sep 2, 2024
1 parent 9ce1059 commit 39d5ba1
Show file tree
Hide file tree
Showing 14 changed files with 445 additions and 126 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package io.quarkus.restclient.config;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import io.quarkus.runtime.configuration.ConfigBuilder;
import io.smallrye.config.ConfigMappingLoader;
import io.smallrye.config.ConfigMappingObject;
import io.smallrye.config.SmallRyeConfigBuilder;

/**
Expand All @@ -25,16 +33,69 @@ public abstract class AbstractRestClientConfigBuilder implements ConfigBuilder {
@Override
public SmallRyeConfigBuilder configBuilder(final SmallRyeConfigBuilder builder) {
List<RegisteredRestClient> restClients = getRestClients();
builder.withInterceptors(new RestClientNameFallbackConfigSourceInterceptor(restClients));
Set<String> ignoreNames = getIgnoreNames();
builder.withInterceptors(new RestClientNameUnquotedFallbackInterceptor(restClients, ignoreNames));
builder.withInterceptors(new RestClientNameFallbackInterceptor(restClients, ignoreNames));
for (RegisteredRestClient restClient : restClients) {
builder.withDefaultValue("quarkus.rest-client.\"" + restClient.getFullName() + "\".force", "true");
builder.withDefaultValue("quarkus.rest-client." + restClient.getSimpleName() + ".force", "true");
if (restClient.getConfigKey() != null) {
builder.withDefaultValue("quarkus.rest-client." + restClient.getConfigKey() + ".force", "true");
builder.withDefaultValue("quarkus.rest-client.\"" + restClient.getConfigKey() + "\".force", "true");
}
}
return builder;
}

public abstract List<RegisteredRestClient> getRestClients();

/**
* Builds a list of base names from {@link RestClientsConfig} to ignore when rewriting the REST Client
* configuration. Only configuration from {@link RestClientsConfig#clients()} requires rewriting, but they share
* the same path of the base names due to {@link io.smallrye.config.WithParentName} in the member.
*
* @return a Set with the names to ignore.
*/
public Set<String> getIgnoreNames() {
Class<? extends ConfigMappingObject> implementationClass = ConfigMappingLoader
.getImplementationClass(RestClientsConfig.class);
return configMappingNames(implementationClass).get(RestClientsConfig.class.getName()).get("")
.stream()
.filter(s -> s.charAt(0) != '*')
.map(s -> "quarkus.rest-client." + s)
.collect(Collectors.toSet());
}

/**
* TODO - Generate this in RestClientConfigUtils - The list can be collected during build time and generated
*/
@SuppressWarnings("unchecked")
@Deprecated(forRemoval = true)
static <T> Map<String, Map<String, Set<String>>> configMappingNames(final Class<T> implementationClass) {
try {
Method getNames = implementationClass.getDeclaredMethod("getNames");
return (Map<String, Map<String, Set<String>>>) getNames.invoke(null);
} catch (NoSuchMethodException e) {
throw new NoSuchMethodError(e.getMessage());
} catch (IllegalAccessException e) {
throw new IllegalAccessError(e.getMessage());
} catch (InvocationTargetException e) {
try {
throw e.getCause();
} catch (RuntimeException | Error e2) {
throw e2;
} catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}
}

static int indexOfRestClient(final String name) {
if (name.startsWith("quarkus.rest-client.")) {
return 20;
}
if (name.startsWith("quarkus.rest-client-reactive.")) {
return 29;
}
return -1;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package io.quarkus.restclient.config;

import io.smallrye.config.NameIterator;

public class RegisteredRestClient {
private final String fullName;
private final String simpleName;
private final String configKey;
private final boolean configKeySegments;

public RegisteredRestClient(final String fullName, final String simpleName) {
this(fullName, simpleName, null);
Expand All @@ -13,6 +16,7 @@ public RegisteredRestClient(final String fullName, final String simpleName, fina
this.fullName = fullName;
this.simpleName = simpleName;
this.configKey = configKey;
this.configKeySegments = configKey != null && new NameIterator(configKey).nextSegmentEquals(configKey);
}

public String getFullName() {
Expand All @@ -26,4 +30,11 @@ public String getSimpleName() {
public String getConfigKey() {
return configKey;
}

public boolean isConfigKeySegments() {
if (configKey == null) {
throw new IllegalStateException("configKey is null");
}
return !configKeySegments;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.quarkus.restclient.config;

import static io.quarkus.restclient.config.AbstractRestClientConfigBuilder.indexOfRestClient;

import java.util.List;
import java.util.Set;
import java.util.function.Function;

import jakarta.annotation.Priority;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import io.smallrye.config.FallbackConfigSourceInterceptor;
import io.smallrye.config.Priorities;
import io.smallrye.config.SmallRyeConfigBuilder;

/**
* Fallbacks REST Client FQN to Simple Name and quoted config keys to unquoted
* <p>
* Ideally, this shouldn't be required. The old custom implementation allowed us to mix both FQN and Simple Name in a
* merged configuration to use in the REST Client. The standard Config system does not support such a feature. If a
* configuration supports multiple names, the user has to use the same name across all configuration sources. No other
* Quarkus extension behaves this way because only the REST Client extension provides the custom code to make it work.
* <p>
* In the case of {@link RegisterRestClient#configKey()}, users either use quoted or unquoted configuration names for
* single config key segments. Again, the Config system does not support such a feature (but could be implemented), so
* the interceptor also fallbacks to unquoted configuration names, due to the <code>force</code> property added by
* {@link AbstractRestClientConfigBuilder#configBuilder(SmallRyeConfigBuilder)}.
*/
@Priority(Priorities.LIBRARY + 610)
public class RestClientNameFallbackInterceptor extends FallbackConfigSourceInterceptor {
public RestClientNameFallbackInterceptor(final List<RegisteredRestClient> restClients,
final Set<String> ignoreNames) {
super(fallback(restClients, ignoreNames));
}

private static Function<String, String> fallback(final List<RegisteredRestClient> restClients,
final Set<String> ignoreNames) {
return new Function<String, String>() {
@Override
public String apply(final String name) {
int indexOfRestClient = indexOfRestClient(name);
if (indexOfRestClient != -1) {
if (ignoreNames.contains(name)) {
return name;
}

int endOfRestClient = indexOfRestClient + 1;
for (RegisteredRestClient restClient : restClients) {
if (name.length() > indexOfRestClient && name.charAt(indexOfRestClient) == '"') {
String interfaceName = restClient.getFullName();
if (name.regionMatches(endOfRestClient, interfaceName, 0, interfaceName.length())) {
if (name.length() > endOfRestClient + interfaceName.length()
&& name.charAt(endOfRestClient + interfaceName.length()) == '"') {
return "quarkus.rest-client." + restClient.getSimpleName()
+ name.substring(endOfRestClient + interfaceName.length() + 1);
}
}

String configKey = restClient.getConfigKey();
if (configKey == null || configKey.isEmpty() || restClient.isConfigKeySegments()) {
continue;
}
int endOfConfigKey = endOfRestClient + configKey.length();
if (name.regionMatches(endOfRestClient, configKey, 0, configKey.length())) {
if (name.length() > endOfConfigKey && name.charAt(endOfConfigKey) == '"') {
return "quarkus.rest-client." + configKey + name.substring(endOfConfigKey + 1);
}
}
}
}
}
return name;
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.quarkus.restclient.config;

import static io.quarkus.restclient.config.AbstractRestClientConfigBuilder.indexOfRestClient;
import static io.smallrye.config.ProfileConfigSourceInterceptor.convertProfile;

import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Function;

import jakarta.annotation.Priority;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import io.smallrye.config.ConfigValue;
import io.smallrye.config.FallbackConfigSourceInterceptor;
import io.smallrye.config.NameIterator;
import io.smallrye.config.Priorities;

/**
* Relocates unquoted config keys to quoted
* <p>
* In the case of {@link RegisterRestClient#configKey()}, users either use quoted or unquoted configuration names for
* single config key segments. Again, the Config system does not support such a feature (but could be implemented), so
* the interceptor also relocates to unquoted configuration names.
* <p>
* We need a double-way relocation / fallback mapping between unquoted and quoted because SmallRye Config will use the
* first distict key it finds to populate {@link RestClientsConfig#clients()} in the list of property names. If quoted,
* it will search for all quoted. If unquoted, it will search for all unquoted. We cannot be sure how the user sets the
* configuration, especially considering that we may not be able to query the list directly if the config comes from a
* source that does not support listing property names.
*/
@Priority(Priorities.LIBRARY + 605)
public class RestClientNameUnquotedFallbackInterceptor extends FallbackConfigSourceInterceptor {
public RestClientNameUnquotedFallbackInterceptor(final List<RegisteredRestClient> restClients,
final Set<String> ignoreNames) {
super(relocate(restClients, ignoreNames));
}

private static Function<String, String> relocate(final List<RegisteredRestClient> restClients,
final Set<String> ignoreNames) {
return new Function<String, String>() {
@Override
public String apply(final String name) {
int indexOfRestClient = indexOfRestClient(name);
if (indexOfRestClient != -1) {
if (ignoreNames.contains(name)) {
return name;
}

for (RegisteredRestClient restClient : restClients) {
String configKey = restClient.getConfigKey();
if (configKey == null || configKey.isEmpty() || restClient.isConfigKeySegments()) {
continue;
}

int endOfConfigKey = indexOfRestClient + configKey.length();
if (name.regionMatches(indexOfRestClient, configKey, 0, configKey.length())) {
if (name.length() > endOfConfigKey && name.charAt(endOfConfigKey) == '.') {
return "quarkus.rest-client.\"" + configKey + "\"" + name.substring(endOfConfigKey);
}
}
}
}
return name;
}
};
}

private static final Comparator<ConfigValue> CONFIG_SOURCE_COMPARATOR = new Comparator<ConfigValue>() {
@Override
public int compare(ConfigValue original, ConfigValue candidate) {
int result = Integer.compare(original.getConfigSourceOrdinal(), candidate.getConfigSourceOrdinal());
if (result != 0) {
return result;
}
result = Integer.compare(original.getConfigSourcePosition(), candidate.getConfigSourcePosition()) * -1;
if (result != 0) {
return result;
}
// If both properties are profiled, prioritize the one with the most specific profile.
if (original.getName().charAt(0) == '%' && candidate.getName().charAt(0) == '%') {
List<String> originalProfiles = convertProfile(
new NameIterator(original.getName()).getNextSegment().substring(1));
List<String> candidateProfiles = convertProfile(
new NameIterator(candidate.getName()).getNextSegment().substring(1));
return Integer.compare(originalProfiles.size(), candidateProfiles.size()) * -1;
}
return result;
}
};
}
Loading

0 comments on commit 39d5ba1

Please sign in to comment.