Skip to content

Commit

Permalink
Support loading additional configuration locations.
Browse files Browse the repository at this point in the history
  • Loading branch information
radcortez committed Nov 12, 2020
1 parent 6d5536d commit 9686aa4
Show file tree
Hide file tree
Showing 20 changed files with 1,044 additions and 12 deletions.
5 changes: 5 additions & 0 deletions implementation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@
<groupId>io.smallrye.testing</groupId>
<artifactId>smallrye-testing-utilities</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap</groupId>
<artifactId>shrinkwrap-impl-base</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package io.smallrye.config;

import static io.smallrye.common.classloader.ClassPathUtils.consumeAsPath;
import static io.smallrye.common.classloader.ClassPathUtils.consumeAsPaths;
import static io.smallrye.config.Converters.newCollectionConverter;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.function.Consumer;

import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.Converter;

import io.smallrye.common.annotation.Experimental;

/**
* This {@code AbstractLocationConfigSourceFactory} allows to initialize additional config locations with the
* configuration {@link AbstractLocationConfigSourceFactory#SMALLRYE_LOCATIONS}. The configuration support multiple
* locations separated by a comma and each much represent a valid {@link URI}.
* <p>
*
* The locations comprise a list of URIs which are loaded in order. The following URI schemes are supported:
*
* <ol>
* <li>file or directory</li>
* <li>classpath resource</li>
* <li>jar resource</li>
* <li>http resource</li>
* </ol>
* <p>
*
* If a profile is active, the profile resource is only loaded if the unprofiled resource is available in the same
* location. This is to keep a consistent loading order and matching with the unprofiled resource. Profiles are not
* taken into account if the location is a directory.
*/
@Experimental("Load additional config locations")
public abstract class AbstractLocationConfigSourceFactory implements ConfigSourceFactory {
public static final String SMALLRYE_LOCATIONS = "smallrye.config.locations";

protected abstract String[] getFileExtensions();

protected abstract ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException;

protected ConfigSource loadConfigSource(final URL url) throws IOException {
return this.loadConfigSource(url, ConfigSource.DEFAULT_ORDINAL);
}

@Override
public Iterable<ConfigSource> getConfigSources(final ConfigSourceContext context) {
final ConfigValue value = context.getValue(SMALLRYE_LOCATIONS);

final List<ConfigSource> configSources = new ArrayList<>();
if (value.getValue() != null) {
List<URI> resources = newCollectionConverter(new URIConverter(), ArrayList::new).convert(value.getValue());
if (resources != null) {
for (URI uri : resources) {
if (uri.getScheme() == null || uri.getScheme().equals("file")) {
tryFileSystem(uri, context, configSources);
tryClassPath(uri, context, configSources);
} else if (uri.getScheme().equals("jar")) {
tryJar(uri, context, configSources);
} else if (uri.getScheme().startsWith("http")) {
tryHttpResource(uri, context, configSources);
} else {
throw ConfigMessages.msg.schemeNotSupported(uri.getScheme());
}
}
}
}

return configSources;
}

private void tryFileSystem(final URI uri, final ConfigSourceContext context, final List<ConfigSource> configSources) {
final Path urlPath = Paths.get(uri.getPath());
if (Files.isRegularFile(urlPath)) {
consumeAsPath(toURL(urlPath.toUri()), new ConfigSourcePathConsumer(context, configSources));
} else if (Files.isDirectory(urlPath)) {
try (DirectoryStream<Path> paths = Files.newDirectoryStream(urlPath, this::validExtension)) {
for (Path path : paths) {
addConfigSource(path.toUri(), configSources);
}
} catch (IOException e) {
throw ConfigMessages.msg.failedToLoadResource(e);
}
}
}

private void tryClassPath(final URI uri, final ConfigSourceContext context, final List<ConfigSource> configSources) {
try {
consumeAsPaths(uri.getPath(), new ConfigSourcePathConsumer(context, configSources));
} catch (IOException e) {
throw ConfigMessages.msg.failedToLoadResource(e);
} catch (IllegalArgumentException e) {
fallbackToUnknownProtocol(uri, configSources);
}
}

private void tryJar(final URI uri, final ConfigSourceContext context, final List<ConfigSource> configSources) {
try {
consumeAsPath(toURL(uri), new ConfigSourcePathConsumer(context, configSources));
} catch (Exception e) {
throw ConfigMessages.msg.failedToLoadResource(e);
}
}

private void fallbackToUnknownProtocol(final URI uri, final List<ConfigSource> configSources) {
final ClassLoader classLoader = SecuritySupport.getContextClassLoader();
try {
Enumeration<URL> resources = classLoader.getResources(uri.toString());
while (resources.hasMoreElements()) {
final URL resourceUrl = resources.nextElement();
if (validExtension(resourceUrl.getFile())) {
addConfigSource(resourceUrl, configSources);
}
}
} catch (IOException e) {
throw ConfigMessages.msg.failedToLoadResource(e);
}
}

private void tryHttpResource(final URI uri, final ConfigSourceContext context, final List<ConfigSource> configSources) {
if (validExtension(uri.getPath())) {
addConfigSource(uri, configSources);
tryProfiles(uri, context, configSources);
}
}

private void tryProfiles(final URI uri, final ConfigSourceContext context, final List<ConfigSource> configSources) {
final List<String> profiles = context.getProfiles();
for (int i = profiles.size() - 1; i >= 0; i--) {
final int lastOrdinal = configSources.get(configSources.size() - 1).getOrdinal();
final URL profileToFileName = toURL(addProfileToFileName(uri, profiles.get(i)));
consumeAsPath(profileToFileName, path -> addProfileConfigSource(profileToFileName, lastOrdinal + 1, configSources));
}
}

private static URL toURL(final URI uri) {
try {
return uri.toURL();
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}

private void addConfigSource(final URI uri, final List<ConfigSource> configSources) {
addConfigSource(toURL(uri), configSources);
}

private void addConfigSource(final URL url, final List<ConfigSource> configSources) {
try {
final ConfigSource configSource = loadConfigSource(url);
if (!configSource.getPropertyNames().isEmpty()) {
configSources.add(configSource);
}
} catch (IOException e) {
throw ConfigMessages.msg.failedToLoadResource(e);
}
}

private void addProfileConfigSource(final URL url, final int ordinal, final List<ConfigSource> configSources) {
try {
final ConfigSource configSource = loadConfigSource(url, ordinal);
if (!configSource.getPropertyNames().isEmpty()) {
configSources.add(configSource);
}
} catch (FileNotFoundException e) {
// It is ok to not find the resource here, because it is an optional profile resource.
} catch (IOException e) {
throw ConfigMessages.msg.failedToLoadResource(e);
}
}

private boolean validExtension(final Path fileName) {
return validExtension(fileName.getFileName().toString());
}

private boolean validExtension(final String resourceName) {
for (String s : getFileExtensions()) {
if (resourceName.endsWith(s)) {
return true;
}
}
return false;
}

private static URI addProfileToFileName(final URI uri, final String profile) {
if (uri.getScheme().equals("jar")) {
return URI.create("jar:" + addProfileToFileName(URI.create(uri.getSchemeSpecificPart()), profile));
}

final String fileName = uri.getPath();
assert fileName != null;

final int dot = fileName.lastIndexOf(".");
final String fileNameProfile;
if (dot != -1) {
fileNameProfile = fileName.substring(0, dot) + "-" + profile + fileName.substring(dot);
} else {
fileNameProfile = fileName + "-" + profile;
}

try {
return new URI(uri.getScheme(),
uri.getAuthority(),
uri.getHost(),
uri.getPort(),
fileNameProfile,
uri.getQuery(),
uri.getFragment());
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
}
}

private static class URIConverter implements Converter<URI> {
private static final long serialVersionUID = -4852082279190307320L;

@Override
public URI convert(final String value) {
try {
return new URI(value);
} catch (URISyntaxException e) {
throw ConfigMessages.msg.uriSyntaxInvalid(e, value);
}
}
}

private class ConfigSourcePathConsumer implements Consumer<Path> {
private final ConfigSourceContext context;
private final List<ConfigSource> configSources;

public ConfigSourcePathConsumer(final ConfigSourceContext context, final List<ConfigSource> configSources) {
this.context = context;
this.configSources = configSources;
}

@Override
public void accept(final Path path) {
final AbstractLocationConfigSourceFactory factory = AbstractLocationConfigSourceFactory.this;
if (factory.validExtension(path.getFileName().toString())) {
factory.addConfigSource(path.toUri(), configSources);
factory.tryProfiles(path.toUri(), context, configSources);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,13 @@ interface ConfigMessages {

@Message(id = 32, value = "Expected a float value, got \"%s\"")
NumberFormatException floatExpected(String value);

@Message(id = 33, value = "Scheme %s not supported")
IllegalArgumentException schemeNotSupported(String scheme);

@Message(id = 34, value = "URI Syntax invalid %s")
IllegalArgumentException uriSyntaxInvalid(@Cause Throwable cause, String uri);

@Message(id = 35, value = "Failed to load resource")
IllegalStateException failedToLoadResource(@Cause Throwable cause);
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ public PropertiesConfigSource(URL url) throws IOException {
super(NAME_PREFIX + url.toString() + "]", ConfigSourceUtil.urlToMap(url));
}

public PropertiesConfigSource(URL url, int ordinal) throws IOException {
super(NAME_PREFIX + url.toString() + "]", ConfigSourceUtil.urlToMap(url), ordinal);
}

public PropertiesConfigSource(Properties properties, String source) {
super(NAME_PREFIX + source + "]", ConfigSourceUtil.propertiesToMap(properties));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.smallrye.config;

import java.io.IOException;
import java.net.URL;

import org.eclipse.microprofile.config.spi.ConfigSource;

public class PropertiesLocationConfigSourceFactory extends AbstractLocationConfigSourceFactory {
@Override
public String[] getFileExtensions() {
return new String[] { "properties" };
}

@Override
protected ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException {
return new PropertiesConfigSource(url, ordinal);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ private static class ConfigSources implements Serializable {
sortInterceptors.addAll(mapSources(sources));
// Add all interceptors
sortInterceptors.addAll(mapInterceptors(interceptors));
sortInterceptors.sort(Comparator.comparingInt(ConfigSourceInterceptorWithPriority::getPriority));
sortInterceptors.sort(null);

// Create the initial chain and init each element with the current context
SmallRyeConfigSourceInterceptorContext current = new SmallRyeConfigSourceInterceptorContext(EMPTY, null);
Expand All @@ -334,7 +334,7 @@ private static class ConfigSources implements Serializable {

// Init all late sources. Late sources are converted to the interceptor API and sorted again
sortInterceptors.addAll(mapLateSources(current, sources, getProfiles(sortInterceptors)));
sortInterceptors.sort(Comparator.comparingInt(ConfigSourceInterceptorWithPriority::getPriority));
sortInterceptors.sort(null);

// Rebuild the chain with the late sources and collect new instances of the interceptors
// The new instance will ensure that we get rid of references to factories and other stuff and keep only
Expand Down Expand Up @@ -365,7 +365,7 @@ private static class ConfigSources implements Serializable {
final List<ConfigSourceInterceptorWithPriority> newInterceptors = Arrays
.asList(sources.getInterceptors().toArray(new ConfigSourceInterceptorWithPriority[oldSize + 1]));
newInterceptors.set(oldSize, new ConfigSourceInterceptorWithPriority(configSource));
newInterceptors.sort(Comparator.comparingInt(ConfigSourceInterceptorWithPriority::getPriority));
newInterceptors.sort(null);

SmallRyeConfigSourceInterceptorContext current = new SmallRyeConfigSourceInterceptorContext(EMPTY, null);
for (ConfigSourceInterceptorWithPriority configSourceInterceptor : newInterceptors) {
Expand All @@ -378,6 +378,7 @@ private static class ConfigSources implements Serializable {
}

private static List<ConfigSourceInterceptorWithPriority> mapSources(final List<ConfigSource> sources) {
ConfigSourceInterceptorWithPriority.raiseLoadPriority();
final List<ConfigSourceInterceptorWithPriority> sourcesWithPriority = new ArrayList<>();
for (ConfigSource source : sources) {
if (!(source instanceof ConfigurableConfigSource)) {
Expand Down Expand Up @@ -418,6 +419,7 @@ private static List<ConfigSourceInterceptorWithPriority> mapLateSources(
}
lateSources.sort(Comparator.comparingInt(ConfigurableConfigSource::getOrdinal));

ConfigSourceInterceptorWithPriority.raiseLoadPriority();
final List<ConfigSourceInterceptorWithPriority> sourcesWithPriority = new ArrayList<>();
for (ConfigurableConfigSource configurableSource : lateSources) {
final List<ConfigSource> configSources = configurableSource.getConfigSources(new ConfigSourceContext() {
Expand Down Expand Up @@ -484,6 +486,7 @@ static class ConfigSourceInterceptorWithPriority implements Comparable<ConfigSou

private final Function<ConfigSourceInterceptorContext, ConfigSourceInterceptor> init;
private final int priority;
private final int loadPriority = loadPrioritySequence--;
private final String name;

private ConfigSourceInterceptor interceptor;
Expand Down Expand Up @@ -528,14 +531,18 @@ ConfigSourceInterceptorWithPriority initialized(final ConfigSourceInterceptorCon
return new ConfigSourceInterceptorWithPriority(this.getInterceptor(context), this.priority, this.name);
}

int getPriority() {
return this.priority;
private static int loadPrioritySequence = 0;
private static int loadPrioritySequenceNumber = 1;

static void raiseLoadPriority() {
loadPrioritySequenceNumber++;
loadPrioritySequence = 1000 * loadPrioritySequenceNumber;
}

@Override
public int compareTo(final ConfigSourceInterceptorWithPriority other) {
int res = Integer.compare(this.priority, other.priority);
return res != 0 ? res : this.name.compareTo(other.name);
return res != 0 ? res : Integer.compare(this.loadPriority, other.loadPriority);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.smallrye.config.PropertiesLocationConfigSourceFactory
Loading

0 comments on commit 9686aa4

Please sign in to comment.