Skip to content

Commit

Permalink
Prototype to support loading additional configuration locations.
Browse files Browse the repository at this point in the history
  • Loading branch information
radcortez committed Nov 11, 2020
1 parent 6d5536d commit 68529cd
Show file tree
Hide file tree
Showing 20 changed files with 1,017 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,237 @@
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.URL;
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 java.util.stream.Stream;

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}.
* <p>
*
* Locations set in {@link AbstractLocationConfigSourceFactory#SMALLRYE_LOCATIONS} are loaded in order and from the
* following resources:
*
* <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 loaded if available and the unprofiled resource is also
* available. This is to keep a consistent order 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<URL> resources = newCollectionConverter(new URLConverter(), ArrayList::new).convert(value.getValue());
if (resources != null) {
for (URL url : resources) {
tryFileSystem(url, configSources, context);
tryClassPath(url, configSources, context);
tryHttpResource(url, configSources, context);
}
}
}

return configSources;
}

private void tryFileSystem(final URL url, final List<ConfigSource> configSources, final ConfigSourceContext context) {
if (url.getProtocol().startsWith("file")) {
// Load single file and profiles
final Path urlPath = Paths.get(url.getFile()).toAbsolutePath().normalize();
if (Files.isRegularFile(urlPath)) {
consumeAsPath(toURL(urlPath.toUri()), new ConfigSourcePathConsumer(this, configSources, context));
} else if (Files.isDirectory(urlPath)) { // Load directory contents
try (Stream<Path> paths = Files.walk(urlPath, 1)) {
paths
.filter(Files::isRegularFile)
.filter(path -> validExtension(path.getFileName().toString()))
.forEach(path -> addConfigSource(toURL(path.toUri()), configSources));
} catch (IOException e) {
throw ConfigMessages.msg.failedToLoadResource(e);
}
}
}
}

private void tryClassPath(final URL url, final List<ConfigSource> configSources, final ConfigSourceContext context) {
if (url.getProtocol().startsWith("file")) {
try {
consumeAsPaths(url.getFile(), new ConfigSourcePathConsumer(this, configSources, context));
} catch (IOException e) {
throw ConfigMessages.msg.failedToLoadResource(e);
} catch (IllegalArgumentException e) {
fallbackToUnknownProtocol(url, configSources);
}
}

if (url.getProtocol().startsWith("jar")) {
consumeAsPath(url, new ConfigSourcePathConsumer(this, configSources, context));
}
}

private void fallbackToUnknownProtocol(final URL url, final List<ConfigSource> configSources) {
final ClassLoader classLoader = SecuritySupport.getContextClassLoader();
try {
Enumeration<URL> resources = classLoader.getResources(url.getFile());
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 URL url, final List<ConfigSource> configSources, final ConfigSourceContext context) {
if (url.getProtocol().startsWith("http")) {
if (validExtension(url.toString())) {
addConfigSource(url, configSources);
tryProfiles(url, configSources, context);
}
}
}

private void tryProfiles(final URL url, final List<ConfigSource> configSources, final ConfigSourceContext context) {
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 = addProfileToFileName(url, profiles.get(i));
consumeAsPath(profileToFileName,
path -> addProfileConfigSource(toURL(path.toUri()), 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 URL url, final List<ConfigSource> configSources) {
try {
final ConfigSource configSource = loadConfigSource(url);
if (!configSource.getProperties().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.getProperties().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 String resourceName) {
for (String s : getFileExtensions()) {
if (resourceName.endsWith(s)) {
return true;
}
}
return false;
}

private static URL addProfileToFileName(final URL fileName, final String profile) {
final int dot = fileName.toString().lastIndexOf(".");
try {
if (dot != -1) {
return new URL(fileName.toString().substring(0, dot) + "-" + profile + fileName.toString().substring(dot));
} else {
return new URL(fileName + "-" + profile);
}
} catch (MalformedURLException e) {
// The original URL was already validated, so this shouldn't happen.
throw new IllegalStateException(e);
}
}

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

@Override
public URL convert(final String value) {
try {
return new URL(value);
} catch (MalformedURLException e) {
return convert("file:" + value);
}
}
}

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

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

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

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

@Message(id = 33, 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 68529cd

Please sign in to comment.