diff --git a/README.md b/README.md index 3dfd21b..8c02d83 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,6 @@ Common Java functionality. The implementations in this library are intended to solve simple problems in simple ways. -* [CachedSupplier](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/cache/CachedSupplier.html) -* [Config](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/config/Config.html) -* [ListUtil](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/ListUtil.html) -* [ResourceUtil](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/ResourceUtil.html) -* [Rx3Util](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/rxjava3/Rx3Util.html) -* [SetUtil](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/SetUtil.html) -* [Stopwatch](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/Stopwatch.html) - ## Usage Gradle: @@ -32,6 +24,164 @@ Maven: ``` +## CachedSupplier + +Wraps a `Supplier` and caches its result, so the underlying logic runs only once. +Useful for expensive operations like opening database connections or loading large datasets. + +```java +Supplier supplier = () -> new DatabaseConnection(); +CachedSupplier cached = new CachedSupplier<>(supplier); + +cached.get(); // Creates connection +cached.get(); // Returns cached connection (no new creation) +``` + +- [Javadoc](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/cache/CachedSupplier.html) +- [Source](https://github.com/autonomouslogic/commons-java/blob/main/src/main/java/com/autonomouslogic/commons/cache/CachedSupplier.java) + +## Collection Utilities + +#### ListUtil + +Utilities for combining lists. Two approaches depending on your needs: + +- **`concat()`** — Combines lists into a read-only view without copying elements. Useful for iteration and searching across multiple lists as one logical sequence. + ```java + List first = List.of("a", "b"); + List second = List.of("c", "d"); + List combined = ListUtil.concat(first, second); // lightweight, no copy + combined.size(); // 4 + combined.get(2); // "c" + ``` + +- **`concatCopy()`** — Copies all elements from multiple lists into a new ArrayList. Useful when you need a mutable list or standalone data. + ```java + List combined = ListUtil.concatCopy(first, second); // new ArrayList with all elements + combined.add("e"); // works, unlike concat() + ``` + +[Javadoc](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/ListUtil.html) | [Source](https://github.com/autonomouslogic/commons-java/blob/main/src/main/java/com/autonomouslogic/commons/ListUtil.java) + +#### SetUtil + +Utilities for combining sets. + +- **`mergeCopy()`** — Merges multiple sets into a new LinkedHashSet. Duplicates are automatically handled. + ```java + Set colors1 = Set.of("red", "blue"); + Set colors2 = Set.of("blue", "green"); + Set merged = SetUtil.mergeCopy(colors1, colors2); // {red, blue, green} + ``` + +- **`addAll()`** — Adds elements from multiple sets to an existing target set (in-place). + ```java + Set existing = new HashSet<>(); + SetUtil.addAll(existing, Set.of("apple"), Set.of("banana")); + ``` + +[Javadoc](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/SetUtil.html) | [Source](https://github.com/autonomouslogic/commons-java/blob/main/src/main/java/com/autonomouslogic/commons/SetUtil.java) + +## Config + +Builder-based configuration reader that parses environment variables into typed values with fallback defaults. +Supports many types out-of-the-box (Integer, Boolean, Duration, URI, etc.) and file-based secret injection. + +```java +// Define configs statically +public class AppConfig { + public static final Config ENVIRONMENT = Config.builder() + .name("ENVIRONMENT") + .type(String.class) + .defaultValue("dev") + .build(); + + public static final Config PORT = Config.builder() + .name("PORT") + .type(Integer.class) + .defaultValue(8080) + .build(); +} + +// Read values +Optional env = AppConfig.ENVIRONMENT.get(); // with default +Integer port = AppConfig.PORT.getRequired(); // throws if not set +``` + +File-based secrets: set `PORT_FILE=/run/secrets/port` to read from a file instead of the `PORT` variable directly. + +- [Javadoc](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/config/Config.html) +- [Source](https://github.com/autonomouslogic/commons-java/blob/main/src/main/java/com/autonomouslogic/commons/config/Config.java) + +## ResourceUtil + +Loads resources (files) from the classpath and throws clear exceptions if resources are not found. +Particularly useful for loading configuration files and test fixtures, with contextual loading for organizing test data by test class. + +```java +// Load from classpath +try (var in = ResourceUtil.loadResource("/config.json")) { + var config = new String(in.readAllBytes()); +} + +// Load test fixtures organized by test class +// For class com.example.MyTest, loads from /com/example/MyTest/data.json +try (var in = ResourceUtil.loadContextual(MyTest.class, "/data.json")) { + var data = new String(in.readAllBytes()); +} +``` + +Throws `FileNotFoundException` with the missing path if a resource doesn't exist, unlike standard Java +resource loading which returns null. Two methods: `loadResource()` for absolute/package-relative paths, +`loadContextual()` for paths relative to a test class's directory. + +- [Javadoc](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/ResourceUtil.html) +- [Source](https://github.com/autonomouslogic/commons-java/blob/main/src/main/java/com/autonomouslogic/commons/ResourceUtil.java) + +## Rx3Util + +RxJava 3 utilities for non-blocking async operations and stream processing. + +**Key methods:** + +- `toSingle()`, `toMaybe()`, `toCompletable()` — Convert CompletionStage to RxJava types (non-blocking, unlike fromFuture) +- `retryWithDelayFlowable()` — Retry streams with configurable delay and error filtering +- `orderedMerge()` — Merge sorted streams while maintaining order +- `zipAllFlowable()` — Zip streams until all complete (vs standard zip which stops at shortest) +- `wrapTransformerErrors()` — Wrap transformer errors with context for debugging +- `windowSort()` — Sort stream items within a sliding window +- `checkOrder()` — Verify stream is strictly ordered, error if not + +- [Javadoc](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/rxjava3/Rx3Util.html) +- [Source](https://github.com/autonomouslogic/commons-java/blob/main/src/main/java/com/autonomouslogic/commons/rxjava3/Rx3Util.java) + +## Stopwatch + +Simple stopwatch for measuring elapsed time with nanosecond precision using `System.nanoTime()`. +Supports multiple start/stop cycles with accumulated time, useful for benchmarking and performance monitoring. + +```java +Stopwatch watch = Stopwatch.start(); +doWork(); +watch.stop(); + +System.out.println("Elapsed: " + watch.getDuration()); + +// Resume measuring and accumulate more time +watch.restart(); +doMoreWork(); +watch.stop(); + +System.out.println("Total: " + watch.getDuration()); // combined time from both cycles +``` + +Key methods: `start()` creates and starts a stopwatch, `stop()` accumulates time, `restart()` resumes measurement, +`getNanos()` gets total nanoseconds, `getDuration()` gets total as Duration. Idempotent: calling stop/restart +multiple times won't cause errors. + +- [Javadoc](https://javadoc.io/doc/com.autonomouslogic.commons/commons-java/latest/com/autonomouslogic/commons/Stopwatch.html) +- [Source](https://github.com/autonomouslogic/commons-java/blob/main/src/main/java/com/autonomouslogic/commons/Stopwatch.java) + ## Other Common Libraries * [Apache Commons](https://commons.apache.org/) diff --git a/src/main/java/com/autonomouslogic/commons/ResourceUtil.java b/src/main/java/com/autonomouslogic/commons/ResourceUtil.java index f18b010..7e8efd1 100644 --- a/src/main/java/com/autonomouslogic/commons/ResourceUtil.java +++ b/src/main/java/com/autonomouslogic/commons/ResourceUtil.java @@ -4,7 +4,30 @@ import java.io.InputStream; /** - * Loads class resources. + * Utility for loading resources (files) from the classpath, with clear error handling. + * + *

Standard Java resource loading via {@link Class#getResourceAsStream(String)} returns null if a resource + * is not found, which can lead to hard-to-debug NullPointerExceptions. ResourceUtil instead throws + * {@link FileNotFoundException} with the missing path, making issues obvious. + * + *

Basic usage: + *

{@code
+ * // Load from classpath root
+ * try (var in = ResourceUtil.loadResource("/config.json")) {
+ *     var config = new String(in.readAllBytes());
+ * }
+ * }
+ * + *

Contextual loading (great for tests): + *

{@code
+ * // For class com.example.MyTest, loads from /com/example/MyTest/data.json
+ * try (var in = ResourceUtil.loadContextual(MyTest.class, "/data.json")) {
+ *     var data = new String(in.readAllBytes());
+ * }
+ * }
+ * + *

Contextual loading is especially useful for organizing test fixtures: store each test's resources + * in a directory named after the test class within your test resources directory. */ public class ResourceUtil { private static final char RESOURCE_SEPARATOR = '/'; @@ -12,21 +35,34 @@ public class ResourceUtil { private ResourceUtil() {} /** - * Loads a resource as an {@link InputStream}, throwing an exception if not found rather than simply returning null. - * @param path the path to load - * @return the input stream for the resource - * @throws FileNotFoundException if not found + * Loads a resource from the classpath root as an InputStream. + * + *

Unlike {@link Class#getResourceAsStream(String)}, this method throws {@link FileNotFoundException} + * if the resource does not exist, rather than returning null. + * + *

Example: {@code loadResource("/config.json")} loads {@code src/main/resources/config.json} + * + * @param path the absolute path to the resource (must start with {@code /}) + * @return the InputStream for the resource + * @throws FileNotFoundException if the resource does not exist */ public static InputStream loadResource(String path) throws FileNotFoundException { return loadResource(ResourceUtil.class, path); } /** - * Loads a resource as an {@link InputStream}, throwing an exception if not found rather than simply returning null. - * @param clazz the class to use for resource loading - * @param path the path to load - * @return the input stream for the resource - * @throws FileNotFoundException if not found + * Loads a resource as an InputStream, resolved relative to a given class. + * + *

Unlike {@link Class#getResourceAsStream(String)}, this method throws {@link FileNotFoundException} + * if the resource does not exist, rather than returning null. + * + *

Example: For class {@code com.example.Foo}, {@code loadResource(Foo.class, "/data.json")} + * loads from {@code /com/example/data.json} on the classpath. + * + * @param clazz the class to use for resource resolution + * @param path the path to the resource, relative to the class's package + * @return the InputStream for the resource + * @throws FileNotFoundException if the resource does not exist */ public static InputStream loadResource(Class clazz, String path) throws FileNotFoundException { var in = clazz.getResourceAsStream(path); @@ -37,16 +73,19 @@ public static InputStream loadResource(Class clazz, String path) throws FileN } /** - * Loads a resource with the assumption that the path will be relative to one constructed from the class package and - * name. - * For instance, if the class com.autonomouslogic.SomeClass is provided, the path loaded will be - * /com/autonomouslogic/SomeClass/{path}. - * This is useful for loading resources during unit tests where you store resources for each test in a directory - * named after the test class. - * @param clazz the class to use for resource loading and contextual path - * @param path the path to load - * @return the input stream for the resource - * @throws FileNotFoundException if not found + * Loads a resource relative to the given class's package directory. + * + *

The path is resolved relative to a directory named after the class's fully-qualified name. + * This is particularly useful for organizing test fixtures by test class. + * + *

Example: {@code loadContextual(com.example.MyTest.class, "/data.json")} loads from + * {@code /com/example/MyTest/data.json} on the classpath. This corresponds to a file at + * {@code src/test/resources/com/example/MyTest/data.json} in your project. + * + * @param clazz the class to use for resource resolution and contextual path construction + * @param path the path relative to the class's contextual directory (typically starts with {@code /}) + * @return the InputStream for the resource + * @throws FileNotFoundException if the resource does not exist */ public static InputStream loadContextual(Class clazz, String path) throws FileNotFoundException { var fullPath = RESOURCE_SEPARATOR + clazz.getCanonicalName().replace('.', RESOURCE_SEPARATOR) + path; diff --git a/src/main/java/com/autonomouslogic/commons/SetUtil.java b/src/main/java/com/autonomouslogic/commons/SetUtil.java index edd9a67..6ab32bb 100644 --- a/src/main/java/com/autonomouslogic/commons/SetUtil.java +++ b/src/main/java/com/autonomouslogic/commons/SetUtil.java @@ -4,25 +4,58 @@ import java.util.Set; import lombok.NonNull; +/** + * Utility methods for working with sets. + * + *

Provides simple operations for merging multiple sets without manually iterating or manually + * calling {@link Set#addAll(java.util.Collection)}. + */ public class SetUtil { private SetUtil() {} /** - * Merges the supplied sets into a {@link LinkedHashSet} instance. - * @param sets the sets to be merged - * @return a set containing all the elements - * @param the set element type + * Merges multiple sets into a new {@link LinkedHashSet}. + * + *

Creates a new LinkedHashSet containing all unique elements from all input sets. + * Iteration order is preserved according to insertion order (LinkedHashSet behavior). + * Duplicates across sets are automatically handled by Set semantics. + * + *

Example: + *

{@code
+	 * Set colors1 = Set.of("red", "blue");
+	 * Set colors2 = Set.of("blue", "green");
+	 * Set merged = SetUtil.mergeCopy(colors1, colors2);
+	 * // Result: {red, blue, green} (no duplicate blue)
+	 * }
+ * + * @param sets the sets to merge (varargs, can be empty) + * @return a new LinkedHashSet containing all unique elements from all input sets + * @param the element type */ public static Set mergeCopy(Set... sets) { return addAll(new LinkedHashSet<>(), sets); } /** - * Adds all the provided sets to a specific set instance. - * @param target the set to copy elements into. - * @param sets the sets to be copied - * @return the target set is returned - * @param the set element type + * Adds all elements from multiple sets into a target set. + * + *

Modifies the target set in place, adding all elements from each input set. + * Useful when you already have a set and want to add elements from other sets without creating + * an intermediate collection. + * + *

Example: + *

{@code
+	 * Set existing = new HashSet<>();
+	 * existing.add("apple");
+	 * SetUtil.addAll(existing, Set.of("banana"), Set.of("cherry"));
+	 * // existing now contains: {apple, banana, cherry}
+	 * }
+ * + * @param target the set to add elements to (modified in place) + * @param sets the sets whose elements should be added (varargs, can be empty) + * @return the target set (for chaining) + * @param the element type + * @throws NullPointerException if target is null */ public static Set addAll(@NonNull Set target, Set... sets) { for (Set set : sets) { diff --git a/src/main/java/com/autonomouslogic/commons/Stopwatch.java b/src/main/java/com/autonomouslogic/commons/Stopwatch.java index 5e82805..7900041 100644 --- a/src/main/java/com/autonomouslogic/commons/Stopwatch.java +++ b/src/main/java/com/autonomouslogic/commons/Stopwatch.java @@ -4,9 +4,40 @@ import lombok.Getter; /** - * Measures elapsed time. - * Internally, time measurement is implemented using System.nanoTime(). - * It will be as accurate as that implementation on whatever JVM it's running on. + * A simple stopwatch for measuring elapsed time with nanosecond precision. + * + *

Uses {@link System#nanoTime()} internally for high-resolution timing. Accuracy depends on the JVM + * and underlying OS, but is generally better than millisecond precision. Suitable for benchmarking, + * performance monitoring, and operation timing within your application. + * + *

Basic usage: + *

{@code
+ * Stopwatch watch = Stopwatch.start();
+ * performSomeWork();
+ * watch.stop();
+ *
+ * System.out.println("Elapsed: " + watch.getDuration());
+ * System.out.println("Elapsed nanos: " + watch.getNanos());
+ * }
+ * + *

Multiple measurements: + *

{@code
+ * Stopwatch watch = Stopwatch.start();
+ * processFirstBatch();
+ * watch.stop();
+ *
+ * // Do other work...
+ *
+ * watch.restart();  // Resume measuring
+ * processSecondBatch();
+ * watch.stop();
+ *
+ * // Total time from both batches
+ * System.out.println("Total: " + watch.getDuration());
+ * }
+ * + *

Note: Calling {@link #stop()} multiple times or {@link #restart()} while already running + * is safe — the stopwatch is idempotent and won't double-count time. */ public class Stopwatch { private long start; @@ -21,15 +52,23 @@ private Stopwatch(long start) { } /** - * Starts a new stopwatch. - * @return the stopwatch + * Creates and starts a new stopwatch. + * + *

The stopwatch begins measuring immediately upon creation. + * + * @return a new running stopwatch */ public static Stopwatch start() { return new Stopwatch(System.nanoTime()); } /** - * Starts a new measurement, which will be added to the total time when stopped next. + * Resumes measurement by starting a new measurement cycle. + * + *

The new measurement time will be added to the accumulated total when stopped. + * If the stopwatch is already running, this call has no effect (idempotent). + * + * @see #stop() */ public void restart() { var now = System.nanoTime(); @@ -41,7 +80,14 @@ public void restart() { } /** - * Stops the current measurement, if one is running. + * Stops the current measurement and accumulates the elapsed time. + * + *

If the stopwatch is not running, this call has no effect (idempotent). + * The total accumulated time can be retrieved via {@link #getNanos()} or {@link #getDuration()}. + * + * @see #restart() + * @see #getNanos() + * @see #getDuration() */ public void stop() { var now = System.nanoTime(); @@ -52,6 +98,11 @@ public void stop() { running = false; } + /** + * Returns the total accumulated time as a {@link Duration}. + * + * @return the accumulated elapsed time + */ public Duration getDuration() { return Duration.ofNanos(nanos); } diff --git a/src/main/java/com/autonomouslogic/commons/cache/CachedSupplier.java b/src/main/java/com/autonomouslogic/commons/cache/CachedSupplier.java index d93df19..1976508 100644 --- a/src/main/java/com/autonomouslogic/commons/cache/CachedSupplier.java +++ b/src/main/java/com/autonomouslogic/commons/cache/CachedSupplier.java @@ -4,8 +4,35 @@ import lombok.RequiredArgsConstructor; /** - * Caches the result of a supplier, ensuring the underlying delegate is only called once. - * @param the type + * Wraps a {@link Supplier} and caches its result, ensuring the underlying delegate is only called once. + * + *

This is useful for lazy initialization of expensive resources. The first call to {@link #get()} invokes the + * delegate, subsequent calls return the cached value without re-executing the supplier logic. + * + *

Example: + *

{@code
+ * // Without caching: expensive operation runs every time
+ * Supplier supplier = () -> {
+ *     System.out.println("Creating connection...");
+ *     return new DatabaseConnection();
+ * };
+ * supplier.get(); // prints "Creating connection..."
+ * supplier.get(); // prints "Creating connection..." again
+ *
+ * // With caching: expensive operation runs only once
+ * CachedSupplier cached = new CachedSupplier<>(supplier);
+ * cached.get(); // prints "Creating connection..."
+ * cached.get(); // returns cached value, no output
+ * }
+ * + *

Features: + *

    + *
  • Thread-unsafe: intended for single-threaded use or when synchronization is handled externally + *
  • Caches null values: if the delegate returns null, that null is cached and returned on future calls + *
  • Simple: no overhead beyond a boolean flag and a field reference + *
+ * + * @param the type of value supplied */ @RequiredArgsConstructor public class CachedSupplier implements Supplier { @@ -14,8 +41,12 @@ public class CachedSupplier implements Supplier { private T value; /** - * Returns the result of the delegate supplier, or the cached value if present. - * @return the supplier value + * Returns the cached value, or calls the delegate supplier on the first invocation. + * + *

First call: invokes the delegate, caches the result (including null), and returns it. + * Subsequent calls: returns the cached value without invoking the delegate. + * + * @return the cached supplier value, or null if the delegate returns null */ @Override public T get() { diff --git a/src/main/java/com/autonomouslogic/commons/collection/ConcatCollection.java b/src/main/java/com/autonomouslogic/commons/collection/ConcatCollection.java index 2c8e99f..f3e2250 100644 --- a/src/main/java/com/autonomouslogic/commons/collection/ConcatCollection.java +++ b/src/main/java/com/autonomouslogic/commons/collection/ConcatCollection.java @@ -6,6 +6,28 @@ import java.util.List; import lombok.RequiredArgsConstructor; +/** + * A read-only view that treats multiple collections as a single collection. + * + *

This class combines multiple collections without copying their elements. It's useful when you need to + * iterate over or search across multiple collections as if they were one, without the overhead of + * creating a new combined collection. + * + *

Note: This is a package-private class. Use {@link com.autonomouslogic.commons.ListUtil#concat(List[])} + * to create a {@link ConcatList} instance instead. + * + *

Supported operations: + *

    + *
  • Read-only: {@code size()}, {@code isEmpty()}, {@code contains()}, {@code iterator()} + *
  • Read-only variants of search methods + *
+ * + *

Unsupported operations: All mutating operations throw {@link UnsupportedOperationException} + * (e.g., {@code add()}, {@code remove()}, {@code clear()}). + * + * @param the type of elements in the collections + * @see ConcatList for a list-specific variant with indexed access + */ @RequiredArgsConstructor abstract class ConcatCollection implements Collection { private final List> collections; diff --git a/src/main/java/com/autonomouslogic/commons/collection/ConcatList.java b/src/main/java/com/autonomouslogic/commons/collection/ConcatList.java index c66f88f..1a8c490 100644 --- a/src/main/java/com/autonomouslogic/commons/collection/ConcatList.java +++ b/src/main/java/com/autonomouslogic/commons/collection/ConcatList.java @@ -7,14 +7,73 @@ import java.util.ListIterator; import java.util.function.UnaryOperator; +/** + * A read-only view that treats multiple lists as a single list. + * + *

Combines multiple lists without copying elements. Useful for accessing multiple lists as a unified + * sequence with indexed access and search operations, without allocating a new collection. + * + *

Usage: Create instances via {@link com.autonomouslogic.commons.ListUtil#concat(List[])} + * rather than calling this constructor directly. + * + *

Example: + *

{@code
+ * import com.autonomouslogic.commons.ListUtil;
+ *
+ * List first = List.of("a", "b");
+ * List second = List.of("c", "d");
+ * ConcatList combined = ListUtil.concat(first, second);
+ *
+ * combined.size();        // 4
+ * combined.get(0);        // "a"
+ * combined.get(2);        // "c"
+ * combined.contains("b"); // true
+ *
+ * for (String item : combined) {
+ *     System.out.println(item); // prints: a, b, c, d
+ * }
+ * }
+ * + *

Supported operations: + *

    + *
  • Indexed access: {@code get(index)}, {@code indexOf()}, {@code lastIndexOf()} + *
  • Iteration: {@code iterator()}, {@code size()}, {@code isEmpty()} + *
  • Searching: {@code contains()} + *
+ * + *

Unsupported operations: All mutating operations throw {@link UnsupportedOperationException} + * (e.g., {@code set()}, {@code add()}, {@code remove()}, {@code sort()}). + * + *

Performance: Operations like {@code get()} traverse lists to find the target index, so + * sequential access via iteration is more efficient than random indexing. + * + * @param the type of elements in the lists + * @see #ConcatList(List) constructor for usage + */ public class ConcatList extends ConcatCollection implements List { private final List> lists; + /** + * Creates a view combining multiple lists. + * + * @param lists a list of lists to combine (the list itself and its contents are defensively copied) + * @throws NullPointerException if {@code lists} or any contained list is null + */ public ConcatList(List> lists) { super(new ArrayList<>(lists)); this.lists = new ArrayList<>(lists); } + /** + * Returns the element at the given index, treating the combined list as a single sequence. + * + *

The index is relative to the start of the concatenated lists. For example, with lists + * {@code [a, b]} and {@code [c, d]}, index 2 returns {@code c}. + * + * @param i the index of the element + * @return the element at the specified index + * @throws IndexOutOfBoundsException if the index is out of bounds + */ @Override public E get(int i) { validateIndex(i); diff --git a/src/main/java/com/autonomouslogic/commons/config/Config.java b/src/main/java/com/autonomouslogic/commons/config/Config.java index 81e2032..34feb13 100644 --- a/src/main/java/com/autonomouslogic/commons/config/Config.java +++ b/src/main/java/com/autonomouslogic/commons/config/Config.java @@ -12,27 +12,59 @@ import lombok.SneakyThrows; /** - * A simple tool for defining and reading configs from environment variables. - * Configs are defined like this: - *

- * {@code
- * public class Configs {
- *     public static final Config VARIABLE_NAME = Config.builder()
- *         .name("VARIABLE_NAME")
+ * A builder-based tool for defining and reading configuration from environment variables with automatic type parsing.
+ *
+ * 

Configurations are typically defined as static constants in a dedicated class, then accessed throughout the + * application. Config handles parsing environment variables into typed values (Integer, Boolean, Duration, etc.) + * and provides fallback defaults. + * + *

Basic usage: + *

{@code
+ * public class AppConfig {
+ *     public static final Config ENVIRONMENT = Config.builder()
+ *         .name("ENVIRONMENT")
  *         .type(String.class)
- *         .defaultValue("dev") // optional
- *         .defaultMethod(() -> "dev") // optional
+ *         .defaultValue("dev")
+ *         .build();
+ *
+ *     public static final Config PORT = Config.builder()
+ *         .name("PORT")
+ *         .type(Integer.class)
+ *         .defaultValue(8080)
  *         .build();
  * }
- * }
- * 
- * And can then be read by {@link #get()} which returns an Optional, - * or by {@link #getRequired()} which throws an exception if not found. * - * A _FILE suffix is also supported for reading config values from files. - * This is useful for storing secrets to avoid them being present directly in the environment. - * In the example above, setting the environment variable VARIABLE_NAME_FILE=/tmp/value.secret would cause - * the contents of /tmp/value.secret to be used instead. + * // Read values + * Optional env = AppConfig.ENVIRONMENT.get(); // with default + * Integer port = AppConfig.PORT.getRequired(); // throws if not set + * }
+ * + *

Supported types: String, Integer, Long, Float, Double, Boolean, BigInteger, BigDecimal, + * LocalDate, Duration, Period, URI. Custom types can be supported by providing a custom {@link ConfigParser}. + * + *

File-based secrets: Environment variables can reference files via a {@code _FILE} suffix. + * This is useful for storing sensitive values without exposing them in the environment: + *

{@code
+ * // Instead of: export DATABASE_PASSWORD=secret123
+ * // Use: export DATABASE_PASSWORD_FILE=/run/secrets/db_password
+ *
+ * Config dbPassword = Config.builder()
+ *     .name("DATABASE_PASSWORD")
+ *     .type(String.class)
+ *     .build();
+ *
+ * // Reading dbPassword.get() will read from /run/secrets/db_password
+ * }
+ * + *

Default values: Use {@code defaultValue()} for static defaults or {@code defaultMethod()} + * for computed defaults. Cannot specify both. + * + *

Error handling: + *

    + *
  • {@link #get()} returns {@link Optional#empty()} if the variable is not set and no default is provided + *
  • {@link #getRequired()} throws {@link IllegalArgumentException} if the variable is missing + *
  • Parsing errors throw {@link IllegalArgumentException} with details about the type and variable name + *
*/ @Builder public class Config { @@ -49,10 +81,34 @@ public class Config { Supplier> defaultMethod; + /** + * Reads the configuration value from the environment or returns a default. + * + *

Resolution order: + *

    + *
  1. Environment variable (parsed to type) + *
  2. File at path specified by {@code _FILE} environment variable + *
  3. Default value (if configured) + *
  4. Default method result (if configured) + *
  5. {@link Optional#empty()} + *
+ * + * @return an Optional containing the parsed value, or empty if not found and no default configured + * @throws IllegalArgumentException if parsing fails or both {@code } and {@code _FILE} are set + */ public Optional get() { return getSetValue().or(this::getDefaultValue); } + /** + * Reads the configuration value from the environment or returns a default. + * + *

Same as {@link #get()}, but throws if the value is not found. + * + * @return the parsed value + * @throws IllegalArgumentException if the value is not set and no default is configured, or if parsing fails, + * or if both {@code } and {@code _FILE} are set + */ public T getRequired() { return get().orElseThrow(() -> new IllegalArgumentException(String.format("No value for %s", name))); } diff --git a/src/main/java/com/autonomouslogic/commons/rxjava3/Rx3Util.java b/src/main/java/com/autonomouslogic/commons/rxjava3/Rx3Util.java index d1ca60f..c57b3cf 100644 --- a/src/main/java/com/autonomouslogic/commons/rxjava3/Rx3Util.java +++ b/src/main/java/com/autonomouslogic/commons/rxjava3/Rx3Util.java @@ -34,14 +34,25 @@ private Rx3Util() {} /** * Converts a {@link CompletionStage} to a {@link Single}. * - * Null return values will result in an error from RxJava, as those aren't allowed. - * Use {@link #toMaybe(CompletionStage)} instead to handle null values properly. + *

Unlike {@link Single#fromFuture(Future)}, this works non-blocking with CompletionStage callbacks. + * Null return values will result in an error, as RxJava Single doesn't allow null. Use {@link #toMaybe(CompletionStage)} + * for null-safe conversions. * - * {@link Single#fromFuture(Future)} works in a blocking fashion, whereas {@link CompletionStage} can be utilised to avoid blocking calls. + *

Example: + *

{@code
+	 * CompletableFuture future = asyncOperation();
+	 * Single single = Rx3Util.toSingle(future);
 	 *
-	 * @param future the completion stage
-	 * @return the Single
-	 * @param  the return parameter of the future
+	 * single.subscribe(
+	 *     value -> System.out.println("Got: " + value),
+	 *     error -> System.err.println("Error: " + error)
+	 * );
+	 * }
+ * + * @param future the completion stage to convert + * @return a Single that emits the completion stage's value + * @param the type of value the completion stage will produce + * @throws NullPointerException if the completion stage completes with a null value */ public static Single toSingle(CompletionStage future) { return Single.create(emitter -> { @@ -67,13 +78,24 @@ public static Single toSingle(CompletionStage future) { /** * Converts a {@link CompletionStage} to a {@link Maybe}. * - * Null return values will result in an empty Maybe. + *

Unlike {@link Maybe#fromFuture(Future)}, this works non-blocking with CompletionStage callbacks. + * Null return values result in an empty Maybe (completion without a value). + * + *

Example: + *

{@code
+	 * CompletableFuture future = asyncOperation();
+	 * Maybe maybe = Rx3Util.toMaybe(future);
 	 *
-	 * {@link Maybe#fromFuture(Future)} works in a blocking fashion, whereas {@link CompletionStage} can be utilised to avoid blocking calls.
+	 * maybe.subscribe(
+	 *     value -> System.out.println("Got: " + value),
+	 *     error -> System.err.println("Error: " + error),
+	 *     () -> System.out.println("Completed without a value")
+	 * );
+	 * }
* - * @param future the completion stage - * @return the Maybe - * @param the return parameter of the future + * @param future the completion stage to convert + * @return a Maybe that emits the completion stage's value, or empty if it completes with null + * @param the type of value the completion stage will produce */ public static Maybe toMaybe(CompletionStage future) { return Maybe.create(emitter -> { @@ -99,10 +121,21 @@ public static Maybe toMaybe(CompletionStage future) { /** * Converts a {@link CompletionStage} to a {@link Completable}. * - * {@link Completable#fromFuture(Future)} works in a blocking fashion, whereas {@link CompletionStage} can be utilised to avoid blocking calls. + *

Unlike {@link Completable#fromFuture(Future)}, this works non-blocking with CompletionStage callbacks. + * + *

Example: + *

{@code
+	 * CompletableFuture future = asyncOperation();
+	 * Completable completable = Rx3Util.toCompletable(future);
 	 *
-	 * @param future the completion stage
-	 * @return the Completable
+	 * completable.subscribe(
+	 *     () -> System.out.println("Operation completed"),
+	 *     error -> System.err.println("Error: " + error)
+	 * );
+	 * }
+ * + * @param future the completion stage to convert + * @return a Completable that completes or errors based on the completion stage */ public static Completable toCompletable(CompletionStage future) { return Completable.create(emitter -> { @@ -166,21 +199,36 @@ public static FlowableTransformer wrapTransformerErrors( } /** - * Merges a number of sources together always picking the next item from the source which compares the lowest. - * In order to merge sources in a completely ordered way, it is assumed the sources are already themselves sorted. - * @param comparator - * @param sources - * @return the merged Publisher - * @param the type of the Publisher to merge + * Merges sorted sources into a single stream while maintaining order. + * + *

Emits items from whichever source has the lowest item according to the comparator, continuing until all + * sources are exhausted. This assumes all input sources are already sorted according to the same comparator. + * + * @param comparator the comparator to determine which source provides the next item + * @param sources the sorted publishers to merge + * @return a Publisher emitting items in sorted order + * @param the type of items emitted by the publishers */ public static Publisher orderedMerge(Comparator comparator, Publisher... sources) { return new OrderedMerger<>(comparator, sources).createPublisher(); } /** - * Like {@link Flowable#zipArray(Function, boolean, int, Publisher[])}, but keeps going until all the sources - * have ended. It does this by wrapping all the values in {@link Optional}s and replacing the ended sources with empty - * ones. + * Zips publishers until all sources have emitted and completed, unlike standard Flowable zip operations + * which stop when the shortest source completes. + * + *

Values are wrapped in {@link Optional} to distinguish between "no value emitted" and "null value". + * Once a source completes, it continues emitting empty Optionals to allow other sources to catch up. + * + *

This is useful when you want to track the latest value from each source even after some have completed. + * + * @param zipper function to combine values from all sources (receives Object array of Optional values) + * @param delayError if true, errors are delayed until all sources complete; if false, errors terminate immediately + * @param bufferSize the buffer size for each source + * @param sources the publishers to zip + * @return a Flowable emitting combined values from all sources + * @param the type of items emitted by the publishers + * @param the type of items emitted by the resulting Flowable */ public static <@NonNull T, @NonNull R> Flowable zipAllFlowable( @NonNull Function zipper, @@ -190,6 +238,16 @@ public static Publisher orderedMerge(Comparator comparator, Publisher< return new ZipAll(zipper, delayError, bufferSize, sources).createFlowable(); } + /** + * Convenience overload of {@link #zipAllFlowable(Function, boolean, int, Publisher[])} with default settings + * (delayError=false, bufferSize=Flowable.bufferSize()). + * + * @param zipper function to combine values from all sources + * @param sources the publishers to zip + * @return a Flowable emitting combined values from all sources + * @param the type of items emitted by the publishers + * @param the type of items emitted by the resulting Flowable + */ public static <@NonNull T, @NonNull R> Flowable zipAllFlowable( @NonNull Function zipper, @NonNull Publisher... sources) { return zipAllFlowable(zipper, false, Flowable.bufferSize(), sources); @@ -212,10 +270,52 @@ public static Publisher orderedMerge(Comparator comparator, Publisher< return new CheckOrder(comparator); } + /** + * Creates a transformer that retries a Flowable a specified number of times with a delay between attempts. + * + *

This is a convenience overload that retries on all errors. Use {@link #retryWithDelayFlowable(int, Duration, Predicate)} + * to selectively retry only certain errors. + * + *

Example: + *

{@code
+	 * Flowable source = apiCall();
+	 * source.compose(Rx3Util.retryWithDelayFlowable(3, Duration.ofSeconds(1)))
+	 *     .subscribe(
+	 *         item -> System.out.println(item),
+	 *         error -> System.err.println("Failed after retries: " + error)
+	 *     );
+	 * }
+ * + * @param times the maximum number of retry attempts (0 means no retries) + * @param delay the delay between retry attempts + * @return a FlowableTransformer that retries with delay on error + * @param the type of items in the Flowable + */ public static FlowableTransformer retryWithDelayFlowable(int times, Duration delay) { return retryWithDelayFlowable(times, delay, e -> true); } + /** + * Creates a transformer that retries a Flowable a specified number of times with a delay between attempts, + * only for errors matching a predicate. + * + *

Example: + *

{@code
+	 * Flowable source = apiCall();
+	 * source.compose(Rx3Util.retryWithDelayFlowable(3, Duration.ofSeconds(1), error -> error instanceof TimeoutException))
+	 *     .subscribe(
+	 *         item -> System.out.println(item),
+	 *         error -> System.err.println("Failed: " + error)
+	 *     );
+	 * }
+ * + * @param times the maximum number of retry attempts (0 means no retries) + * @param delay the delay between retry attempts + * @param predicate determines whether to retry for a given error + * @return a FlowableTransformer that retries with delay on matching errors + * @param the type of items in the Flowable + * @throws IllegalArgumentException if times is negative or delay is negative + */ public static FlowableTransformer retryWithDelayFlowable( int times, Duration delay, Predicate predicate) { if (times < 0) {