Skip to content

Commit

Permalink
Classpaths schema support (aka classpath*)
Browse files Browse the repository at this point in the history
  • Loading branch information
agentgt committed Jan 10, 2025
1 parent 49af634 commit 142cf1c
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 18 deletions.
43 changes: 41 additions & 2 deletions doc/overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,13 @@ <h4 id="resource-media-type">Resource Media Type</h4>

<h4 id="resource-filters">Resource Filters</h4>
<p>Filters can be applied to a resource to alter the key values after they are loaded and are <em>applied before being parsed
for more resources to load</em> and before being added to the final results.
The ordering of filters matters, so it is generally recommended to use the URI notation, as the order is guaranteed.</p>
for more resources to load</em> and before being added to the final results.
Thus if the resource has child resources the filters are not applied to the childs key values when they are loaded.
Only the resource's key values are filtered.
</p>
<p>
The ordering of filters matters, so it is generally recommended to use the URI notation, as the order is guaranteed.
</p>

<p>The current filters provided out-of-the-box (OOB) are:</p>
<ul>
Expand Down Expand Up @@ -385,6 +390,7 @@ <h3 id="keyvaluesloader">Resource Loading</h3>

<ul>
<li>{@value io.jstach.ezkv.kvs.KeyValuesResource#SCHEMA_CLASSPATH} - classpath resource</li>
<li>{@value io.jstach.ezkv.kvs.KeyValuesResource#SCHEMA_CLASSPATHS} - merge multiple classpath resource with same name.</li>
<li>{@value io.jstach.ezkv.kvs.KeyValuesResource#SCHEMA_FILE} - file resource</li>
<li>{@value io.jstach.ezkv.kvs.KeyValuesResource#SCHEMA_SYSTEM} - System properties</li>
<li>{@value io.jstach.ezkv.kvs.KeyValuesResource#SCHEMA_ENV} - Environment variables</li>
Expand Down Expand Up @@ -506,6 +512,39 @@ <h4 id="uri-schema-provider">URI schema: <code>provider</code></h4>
See {@link io.jstach.ezkv.kvs.KeyValuesServiceProvider.KeyValuesProvider}
for more details on proper usage.

<h4 id="uri-schemas-classpaths">URI schema: <code>classpaths</code></h4>

The {@value io.jstach.ezkv.kvs.KeyValuesResource#SCHEMA_CLASSPATHS} (notice the "s" on the end) schema allows loading multiple resources
found on the class/modulepath with the same name.

<p>
It analogous to Spring's <code>classpath*</code>
support (<code>classpath*</code> is not a valid URI schema). However unlike Spring Ant wild card support
is not currently provided.
</p>
<p>
It can also be used to mimic <a href="https://github.com/lightbend/config?tab=readme-ov-file#standard-behavior">
Lightbend/Typesafe Config <code>reference.conf</code></a> support by <code>classpaths:/reference.conf</code>
</p>

<strong>
It is highly recommended you try not use this and is provided to support mimicing other configuration
systems.
</strong>

<ul>
<li>It is unlikely to work in "uber jars" created by Maven Shade or similar
<a href="https://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html">without proper config</a>.</li>
<li>It has issues in GraalVM native</li>
<li>The ordering is not reliable as it is based on the order of the classpath</li>
<li>In a modular environment encapsulation issues maybe present</li>
<li>For security reasons the found resources are not allowed to load children.</li>
<li><strong>But most of all it is incredibly slow!</strong></li>
</ul>

Instead a more reliable option is to use the <a href="#uri-schema-provider">provider</a> support or just plain <code>classpath</code> (no "s" on end).


<h3 id="keyvaluesmedia">KeyValuesMedia</h3>

<p>{@link io.jstach.ezkv.kvs.KeyValuesMedia}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,27 @@
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Stream;

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import io.jstach.ezkv.kvs.KeyValuesEnvironment.ResourceStreamLoader;
import io.jstach.ezkv.kvs.KeyValuesEnvironment.ResourceLoader;
import io.jstach.ezkv.kvs.KeyValuesMedia.Parser;
import io.jstach.ezkv.kvs.KeyValuesServiceProvider.KeyValuesLoaderFinder;

Expand Down Expand Up @@ -85,6 +90,64 @@ protected KeyValues load(LoaderContext context, KeyValuesResource resource) thro
return load(context, resource, parser);
}
},
CLASSPATHS(KeyValuesResource.SCHEMA_CLASSPATHS) {
@Override
protected KeyValues load(LoaderContext context, KeyValuesResource resource) throws IOException {
// var parser = context.requireParser(resource);
// var logger = context.environment().getLogger();
URI u = resource.uri();
String path = normalizePath(u);
if (path.isBlank()) {
throw new MalformedURLException("Classpaths scheme URI requires a path. URI: " + u);
}
List<URL> urls = context.environment().getResourceLoader().getResources(path).toList();

/*
* Ok so what we are doing here is creating resource key values to load the
* providers just like how profiles work.
*/
List<KeyValuesResource> childResources = new ArrayList<>();
String name = resource.name();

// dedupe resource URLs.
// yes the classloader will give you duplicates.
Set<URI> foundURIs = new HashSet<>();
int i = 0;
for (var url : urls) {
// We change the name and the URI but retain other
// resource config (inherited).
var builder = resource.toBuilder();
URI uri;
try {
uri = url.toURI();
}
catch (URISyntaxException e) {
throw new IOException("Unsupported resource URL: " + url, e);
}
if (!foundURIs.add(uri)) {
continue;
}
builder.name(name + i++);
builder.uri(uri);
/*
* We don't allow loading of children for security reasons.
*/
builder._addFlag(LoadFlag.NO_LOAD_CHILDREN);
childResources.add(builder.build());
}
return childResources(context, resource, childResources);

// var b = KeyValues.builder(resource);
// for (var url : urls) {
// logger.debug("Classpaths loading: " + url);
// try (var is = url.openStream()) {
// parser.parse(is, b::add);
// }
// }
// return b.build();

}
},
FILE(KeyValuesResource.SCHEMA_FILE) {
@Override
protected KeyValues load(LoaderContext context, KeyValuesResource resource) throws IOException {
Expand Down Expand Up @@ -189,6 +252,36 @@ protected KeyValues load(LoaderContext context, KeyValuesResource resource) thro
return maybeUseKeyFromUri(context, resource, kvs);
}
},
JAR("jar") {
@Override
protected KeyValues load(LoaderContext context, KeyValuesResource resource) throws IOException {
return loadURL(context, resource);
}
},
JRT("jrt") {
@Override
protected KeyValues load(LoaderContext context, KeyValuesResource resource) throws IOException {
return loadURL(context, resource);
}
},
VFS("vfs") {
@Override
protected KeyValues load(LoaderContext context, KeyValuesResource resource) throws IOException {
return loadURL(context, resource);
}
},
VFSZIP("vfszip") {
@Override
protected KeyValues load(LoaderContext context, KeyValuesResource resource) throws IOException {
return loadURL(context, resource);
}
},
BUNDLE("bundle") {
@Override
protected KeyValues load(LoaderContext context, KeyValuesResource resource) throws IOException {
return loadURL(context, resource);
}
},

;

Expand Down Expand Up @@ -224,7 +317,7 @@ protected KeyValuesLoader loader(LoaderContext context, KeyValuesResource resour
protected KeyValues load(LoaderContext context, KeyValuesResource resource, Parser parser) throws IOException {
var fileSystem = context.environment().getFileSystem();
var cwd = context.environment().getCWD();
var is = openURI(resource.uri(), context.environment().getResourceStreamLoader(), fileSystem, cwd);
var is = openURI(resource.uri(), context.environment().getResourceLoader(), fileSystem, cwd);
return parser.parse(resource, is);
}

Expand Down Expand Up @@ -332,7 +425,16 @@ else if (p.startsWith("/")) {
return p;
}

static InputStream openURI(URI u, ResourceStreamLoader loader, FileSystem fileSystem, @Nullable Path cwd)
static KeyValues loadURL(LoaderContext context, KeyValuesResource resource)
throws MalformedURLException, IOException {
URL url = resource.uri().toURL();
var parser = context.requireParser(resource);
try (var is = url.openStream()) {
return parser.parse(resource, is);
}
}

static InputStream openURI(URI u, ResourceLoader loader, FileSystem fileSystem, @Nullable Path cwd)
throws IOException {
if ("classpath".equals(u.getScheme())) {
String path = u.getPath();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import java.io.InputStream;
import java.lang.System.Logger.Level;
import java.net.URI;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.stream.Stream;

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
Expand All @@ -25,6 +28,9 @@
* <p>
* Implementations can replace default system behaviors, enabling custom retrieval of
* environment variables or properties, or integrating custom logging mechanisms.
*
* @apiNote The API in this class uses traditional getter methods because the methods are
* often dynamic and to be consistent with the methods they are facading.
*/
public interface KeyValuesEnvironment {

Expand Down Expand Up @@ -91,16 +97,22 @@ default Logger getLogger() {
}

/**
* Retrieves the {@link ResourceStreamLoader} used for loading resources as streams.
* Retrieves the {@link ResourceLoader} used for loading resources as streams.
* @return the resource stream loader instance
*/
default ResourceStreamLoader getResourceStreamLoader() {
return new ResourceStreamLoader() {
default ResourceLoader getResourceLoader() {
return new ResourceLoader() {

@Override
public @Nullable InputStream getResourceAsStream(String path) throws IOException {
return getClassLoader().getResourceAsStream(path);
}

@Override
public Stream<URL> getResources(String path) throws IOException {
var cl = getClassLoader();
return Collections.list(cl.getResources(path)).stream();
}
};
}

Expand All @@ -121,7 +133,7 @@ default FileSystem getFileSystem() {
}

/**
* Retrieves the system class loader. By default, delegates to
* Retrieves the class loader. By default, delegates to
* {@link ClassLoader#getSystemClassLoader()}.
* @return the system class loader
*/
Expand All @@ -130,9 +142,9 @@ default ClassLoader getClassLoader() {
}

/**
* Interface for loading resources as input streams.
* Interface for loading resources.
*/
public interface ResourceStreamLoader {
public interface ResourceLoader {

/**
* Retrieves an input stream for the specified resource path.
Expand All @@ -142,6 +154,15 @@ public interface ResourceStreamLoader {
*/
public @Nullable InputStream getResourceAsStream(String path) throws IOException;

/**
* Retrieves classpath resources basically equilvant to
* {@link ClassLoader#getResources(String)}.
* @param path see {@link ClassLoader#getResources(String)}.
* @return a stream of urls.
* @throws IOException if an IO error happens getting the resources URLs.
*/
public Stream<URL> getResources(String path) throws IOException;

/**
* Opens an input stream for the specified resource path. Throws a
* {@link FileNotFoundException} if the resource is not found.
Expand Down
24 changes: 21 additions & 3 deletions ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;

Expand Down Expand Up @@ -58,7 +59,9 @@ public final class Builder implements KeyValuesLoader {

final List<Function<KeyValuesEnvironment, ? extends Variables>> variables = new ArrayList<>();

int resourceCount = 0;
private int resourceCount = 0;

private String namePrefix = "root";

Builder(Function<Builder, KeyValuesLoader> loaderFactory) {
super();
Expand Down Expand Up @@ -101,15 +104,19 @@ public Builder add(String name, KeyValues keyValues) {

/**
* Adds a {@link URI} as a source by wrapping it in a {@link KeyValuesResource}.
* The name of the resource will be automatically generated based on
* {@link #namePrefix(String)} and a counter.
* @param uri the URI to add
* @return this builder instance
*/
public Builder add(URI uri) {
return add(KeyValuesResource.builder(uri).name("resource" + resourceCount).build());
return add(KeyValuesResource.builder(uri).name(namePrefix + resourceCount).build());
}

/**
* Adds a URI specified as a string as a source to the loader.
* Adds a URI specified as a string as a source to the loader. The name of the
* resource will be automatically generated based on {@link #namePrefix(String)}
* and a counter.
* @param uri the URI string to add
* @return this builder instance
*/
Expand Down Expand Up @@ -156,6 +163,17 @@ public Builder resource(Function<KeyValuesEnvironment, KeyValuesResource> resour
return this;
}

/**
* Sets the resource name prefix for auto naming of resources based on a counter.
* This is to support the add methods that do not specify a resource name.
* @param namePrefix must follow {@value KeyValuesResource#RESOURCE_NAME_REGEX}
* @return by default <code>root</code>.
*/
public Builder namePrefix(String namePrefix) {
this.namePrefix = KeyValuesSource.validateName(namePrefix);
return this;
}

/**
* Builds and returns a new {@link KeyValuesLoader} based on the current state of
* the builder.
Expand Down
Loading

0 comments on commit 142cf1c

Please sign in to comment.