diff --git a/doc/overview.html b/doc/overview.html index ebe9619..6a03b08 100644 --- a/doc/overview.html +++ b/doc/overview.html @@ -438,6 +438,34 @@

URI schema: stdin

The above example is particularly useful for passwords, similar to how Docker handles passwords from stdin.

+ +Stdin is retrieved from {@link io.jstach.ezkv.kvs.KeyValuesEnvironment#getStandardInput()}. +Because checking stdin will block an application one must enable reading from it which is done +by either: +
    +
  1. Checking resource parameter of {@value io.jstach.ezkv.kvs.KeyValuesResource#SCHEMA_STDIN_PARAM} is true. +
  2. +
  3. Setting {@value io.jstach.ezkv.kvs.KeyValuesResource#SCHEMA_STDIN_MAIN_ARG_PARAM} to +{@linkplain io.jstach.ezkv.kvs.KeyValuesEnvironment#getMainArgs() command line argument} to check if present. +
  4. +
  5. +Relying on the default which will check "--" + {@link io.jstach.ezkv.kvs.KeyValuesResource#name()} +command line argument is present. +
  6. +
+ +{@snippet lang=properties : +# will be enabled if --dbpassword is passed on the command line +# since dbpassword is the resource name. +_load_dbpassword=stdin:///db.password?_flag=sensitive,optional +# specify that the "--password" command line arg is required +_load_stdin=stdin:///db.password?_param_stdin_arg=--password&_flag=sensitive +# alternatively one can do: +_load_stdin=stdin:///?_p_stdin=${some_other_variable} +# which will be enabled if some_other_variable evalutes to 'true' +} + +

URI schema: profile.

Note: This schema may be renamed to profiles in the future.

diff --git a/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/DefaultKeyValuesLoaderFinder.java b/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/DefaultKeyValuesLoaderFinder.java index b92a564..efd40b4 100644 --- a/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/DefaultKeyValuesLoaderFinder.java +++ b/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/DefaultKeyValuesLoaderFinder.java @@ -12,7 +12,6 @@ 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; @@ -179,6 +178,33 @@ protected KeyValues load(LoaderContext context, KeyValuesResource resource) thro STDIN(KeyValuesResource.SCHEMA_STDIN) { @Override protected KeyValues load(LoaderContext context, KeyValuesResource resource) throws IOException { + + boolean enabled; + + String stdinEnabled = resource.parameters().getValue(KeyValuesResource.SCHEMA_STDIN_PARAM); + + if (stdinEnabled != null) { + enabled = Boolean.parseBoolean(stdinEnabled); + } + else { + String mainArgName = resource.parameters().getValue(KeyValuesResource.SCHEMA_STDIN_MAIN_ARG_PARAM); + + if (mainArgName == null) { + mainArgName = "--" + resource.name(); + } + String argName = mainArgName; + + enabled = Arrays.asList(context.environment().getMainArgs()) + .stream() + .filter(s -> s.equals(argName)) + .findFirst() + .isPresent(); + } + + if (!enabled) { + throw new FileNotFoundException("stdin not enabled."); + } + var builder = KeyValues.builder(resource); BiConsumer consumer = builder::add; diff --git a/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesEnvironment.java b/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesEnvironment.java index ce5218f..379c9cc 100644 --- a/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesEnvironment.java +++ b/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesEnvironment.java @@ -9,7 +9,10 @@ import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Random; @@ -34,6 +37,7 @@ */ public interface KeyValuesEnvironment { + // TODO remove /** * If the loader builder is not passed any resources this resource will be used. * @return default resource is classpath:/boot.properties @@ -47,7 +51,32 @@ default KeyValuesResource defaultResource() { * @return an array of main method arguments */ default @NonNull String[] getMainArgs() { - return new @NonNull String[] {}; + var logger = getLogger(); + logger.warn("Main Args were requested but not provided. Using fallback 'sun.java.command'"); + String value = getSystemProperties().getProperty("sun.java.command"); + if (value == null) { + throw new IllegalStateException("Cannot get main args from 'sun.java.command' as it was not provided"); + } + return fallbackMainArgs(value); + } + + private static @NonNull String[] fallbackMainArgs(String sunJavaCommandValue) { + + // Use fallback by extracting from system property + String command = sunJavaCommandValue; + if (!command.isEmpty()) { + // Split command into components (main class/jar is the first token) + List components = new ArrayList<>(Arrays.asList(command.split("\\s+"))); + // Remove the first element (main class or jar) + if (!components.isEmpty()) { + components.remove(0); + } + // Return remaining as the main args + return components.toArray(new @NonNull String[0]); + } + + // Return an empty array if no arguments are available + return new String[0]; } /** @@ -356,16 +385,15 @@ public void warn(String message) { } -final class DefaultKeyValuesEnvironment implements KeyValuesEnvironment { - - // private static final ThreadLocal threadLocal = new - // ThreadLocal<>(); - // - // static Logger localLogger() { - // var logger = threadLocal.get(); - // if (logger == null) { - // return NoOpLogger.NOPLOGGER; - // } - // } +record DefaultKeyValuesEnvironment(@NonNull String @Nullable [] mainArgs) implements KeyValuesEnvironment { + + @Override + public @NonNull String[] getMainArgs() { + var args = mainArgs; + if (args == null) { + return KeyValuesEnvironment.super.getMainArgs(); + } + return args; + } } diff --git a/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesResource.java b/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesResource.java index 0842e06..b7fd058 100644 --- a/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesResource.java +++ b/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesResource.java @@ -486,17 +486,49 @@ public sealed interface KeyValuesResource extends NamedKeyValuesSource, KeyValue *

* stdin:/// can also bind the input content, parsed as a UTF-8 string, * to the key provided in the URI path. + *

+ * Stdin is retrieved from {@link KeyValuesEnvironment#getStandardInput()}. + * Because checking stdin will block an application one must enable reading from it which is done + * by either: + *

    + *
  1. Checking resource parameter of {@value #SCHEMA_STDIN_PARAM} is true. + *
  2. + *
  3. Setting {@value #SCHEMA_STDIN_MAIN_ARG_PARAM} to + * {@linkplain KeyValuesEnvironment#getMainArgs() command line argument} to check if present. + *
  4. + *
  5. + * Relying on the default which will check -- {@link KeyValuesResource#name()} + * command line argument is present. + *
  6. + *
* * {@snippet lang=properties : + * # will be enabled if --stdin is passed on the command line + * # since stdin in is the resource name. * _load_stdin=stdin:///db.password?_flag=sensitive,optional + * # alternatively one can do: + * _load_stdin=stdin:///?_p_stdin=${some_other_variable} * } - * - * Stdin is retrieved from {@link KeyValuesEnvironment#getStandardInput()}. - * + * @see #SCHEMA_STDIN_PARAM + * @see #SCHEMA_STDIN_MAIN_ARG_PARAM + * @see KeyValuesEnvironment#getMainArgs() */ // @formatter:on public static final String SCHEMA_STDIN = "stdin"; + /** + * Resource parameter to enable stdin. + * @see #SCHEMA_STDIN + */ + public static final String SCHEMA_STDIN_PARAM = "stdin"; + + /** + * Resource parameter to decide what command line argument should be present to allow + * reading from stdin. + * @see #SCHEMA_STDIN + */ + public static final String SCHEMA_STDIN_MAIN_ARG_PARAM = "stdin_arg"; + /** * Will load multiple resources based on a CSV of profiles where the profile name * replaces part of the URI. diff --git a/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesSystem.java b/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesSystem.java index 0b9aa21..1b58b4b 100644 --- a/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesSystem.java +++ b/ezkv-kvs/src/main/java/io/jstach/ezkv/kvs/KeyValuesSystem.java @@ -5,10 +5,12 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; import java.util.function.Function; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import io.jstach.ezkv.kvs.KeyValuesServiceProvider.KeyValuesFilter; @@ -106,13 +108,28 @@ default void close() { /** * Returns a default implementation of {@code KeyValuesSystem} configured with * standard settings and a ServiceLoader based on {@link KeyValuesServiceProvider} and - * its classloader. + * its classloader. If possible prefer {@link #defaults(String[])} to provide command + * line arguments. * @return the default {@code KeyValuesSystem} instance */ public static KeyValuesSystem defaults() { return builder().useServiceLoader().build(); } + /** + * Returns a default implementation of {@code KeyValuesSystem} configured with + * standard settings and a ServiceLoader based on {@link KeyValuesServiceProvider} and + * its classloader. + * @param mainArgs arguments that come from + * public static void main(String [] args). + * @return the default {@code KeyValuesSystem} instance + */ + public static KeyValuesSystem defaults(@NonNull String[] mainArgs) { + Objects.requireNonNull(mainArgs); + var env = new DefaultKeyValuesEnvironment(mainArgs); + return builder().environment(env).useServiceLoader().build(); + } + /** * Creates and returns a new {@link Builder} for constructing customized * {@code KeyValuesSystem} instances. @@ -240,7 +257,7 @@ public Builder useServiceLoader() { public KeyValuesSystem build() { var environment = this.environment; if (environment == null) { - environment = new DefaultKeyValuesEnvironment(); + environment = new DefaultKeyValuesEnvironment(null); } /* * We copy as we are about to use the service loader to add more entries and diff --git a/ezkv-kvs/src/test/java/io/jstach/ezkv/kvs/KeyValuesSystemTest.java b/ezkv-kvs/src/test/java/io/jstach/ezkv/kvs/KeyValuesSystemTest.java index b990c67..d0b9d4a 100644 --- a/ezkv-kvs/src/test/java/io/jstach/ezkv/kvs/KeyValuesSystemTest.java +++ b/ezkv-kvs/src/test/java/io/jstach/ezkv/kvs/KeyValuesSystemTest.java @@ -3,8 +3,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; +import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.PrintStream; import java.net.URI; import java.nio.file.Path; @@ -15,6 +17,7 @@ import java.util.Properties; import java.util.stream.Collectors; +import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.Test; import io.jstach.ezkv.kvs.KeyValuesEnvironment.Logger; @@ -45,6 +48,10 @@ void testLoader() throws FileNotFoundException, IOException { var logger = new TestLogger(); + String stdin = """ + stdin_password=guest + """; + var environment = new KeyValuesEnvironment() { @Override public Properties getSystemProperties() { @@ -56,6 +63,16 @@ public Logger getLogger() { return logger; } + @Override + public InputStream getStandardInput() { + return new ByteArrayInputStream(stdin.getBytes()); + } + + @Override + public @NonNull String[] getMainArgs() { + return new String[] { "--passwords" }; + } + }; var system = KeyValuesResource.builder(URI.create("system:///")) .name("system") @@ -93,6 +110,7 @@ public Logger getLogger() { .add(system) .add("classpath:/test-props/testLoader.properties") .add("classpaths:/classpathstar.properties") + .add("stdin:///?_p_stdin_arg=--passwords&_mime=properties&_flag=sensitive") .add("extra", KeyValues.builder().add(map.entrySet()).build()) .load(); @@ -113,6 +131,7 @@ public Logger getLogger() { sensitive_ab=REDACTED mypassword=REDACTED classpathstar=ezkv + stdin_password=REDACTED fromMap1=1 fromMap2=2 ] @@ -135,6 +154,7 @@ public Logger getLogger() { sensitive_ab=ab mypassword=1.2.3.4.5 classpathstar=ezkv + stdin_password=guest fromMap1=1 fromMap2=2 """; @@ -162,6 +182,7 @@ public Logger getLogger() { KeyValue[key='sensitive_ab', raw='REDACTED', expanded='REDACTED', source=Source[uri=classpath:/test-props/testLoader-sensitive.properties, reference=[key='_load_luggage', in='classpath:/test-props/testLoader.properties'], index=2]] KeyValue[key='mypassword', raw='REDACTED', expanded='REDACTED', source=Source[uri=classpath:/test-props/testLoader-sensitive.properties, reference=[key='_load_luggage', in='classpath:/test-props/testLoader.properties'], index=3]] KeyValue[key='classpathstar', raw='ezkv', expanded='ezkv', source=Source[uri=file:{{CWD}}/target/test-classes/classpathstar.properties, reference=[key='_load_root30', in='classpaths:/classpathstar.properties'], index=1]] + KeyValue[key='stdin_password', raw='REDACTED', expanded='REDACTED', source=Source[uri=stdin:///, index=1]] KeyValue[key='fromMap1', raw='1', expanded='1', source=Source[uri=null:///extra, index=0]] KeyValue[key='fromMap2', raw='2', expanded='2', source=Source[uri=null:///extra, index=0]] """ @@ -204,6 +225,8 @@ public Logger getLogger() { [INFO ] Loaded uri='classpaths:/classpathstar.properties' [DEBUG] Loading uri='file:{{CWD}}/target/test-classes/classpathstar.properties' flags=[NO_LOAD_CHILDREN] specified with key: '_load_root30' in uri='classpaths:/classpathstar.properties' [INFO ] Loaded uri='file:{{CWD}}/target/test-classes/classpathstar.properties' flags=[NO_LOAD_CHILDREN] + [DEBUG] Loading uri='stdin:///' flags=[SENSITIVE] + [INFO ] Loaded uri='stdin:///' flags=[SENSITIVE] """ .replace("{{CWD}}", cwd); assertEquals(expected, actual);