Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 158 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -32,6 +24,164 @@ Maven:
</dependency>
```

## 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<DatabaseConnection> supplier = () -> new DatabaseConnection();
CachedSupplier<DatabaseConnection> 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<String> first = List.of("a", "b");
List<String> second = List.of("c", "d");
List<String> 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<String> 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<String> colors1 = Set.of("red", "blue");
Set<String> colors2 = Set.of("blue", "green");
Set<String> merged = SetUtil.mergeCopy(colors1, colors2); // {red, blue, green}
```

- **`addAll()`** — Adds elements from multiple sets to an existing target set (in-place).
```java
Set<String> 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<String> ENVIRONMENT = Config.<String>builder()
.name("ENVIRONMENT")
.type(String.class)
.defaultValue("dev")
.build();

public static final Config<Integer> PORT = Config.<Integer>builder()
.name("PORT")
.type(Integer.class)
.defaultValue(8080)
.build();
}

// Read values
Optional<String> 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/)
Expand Down
79 changes: 59 additions & 20 deletions src/main/java/com/autonomouslogic/commons/ResourceUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,65 @@
import java.io.InputStream;

/**
* Loads class resources.
* Utility for loading resources (files) from the classpath, with clear error handling.
*
* <p>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.
*
* <p><b>Basic usage:</b>
* <pre>{@code
* // Load from classpath root
* try (var in = ResourceUtil.loadResource("/config.json")) {
* var config = new String(in.readAllBytes());
* }
* }</pre>
*
* <p><b>Contextual loading (great for tests):</b>
* <pre>{@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());
* }
* }</pre>
*
* <p>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 = '/';

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.
*
* <p>Unlike {@link Class#getResourceAsStream(String)}, this method throws {@link FileNotFoundException}
* if the resource does not exist, rather than returning null.
*
* <p>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.
*
* <p>Unlike {@link Class#getResourceAsStream(String)}, this method throws {@link FileNotFoundException}
* if the resource does not exist, rather than returning null.
*
* <p>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);
Expand All @@ -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 <code>com.autonomouslogic.SomeClass</code> is provided, the path loaded will be
* <code>/com/autonomouslogic/SomeClass/{path}</code>.
* 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.
*
* <p>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.
*
* <p>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;
Expand Down
51 changes: 42 additions & 9 deletions src/main/java/com/autonomouslogic/commons/SetUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,58 @@
import java.util.Set;
import lombok.NonNull;

/**
* Utility methods for working with sets.
*
* <p>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 <E> the set element type
* Merges multiple sets into a new {@link LinkedHashSet}.
*
* <p>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.
*
* <p><b>Example:</b>
* <pre>{@code
* Set<String> colors1 = Set.of("red", "blue");
* Set<String> colors2 = Set.of("blue", "green");
* Set<String> merged = SetUtil.mergeCopy(colors1, colors2);
* // Result: {red, blue, green} (no duplicate blue)
* }</pre>
*
* @param sets the sets to merge (varargs, can be empty)
* @return a new LinkedHashSet containing all unique elements from all input sets
* @param <E> the element type
*/
public static <E> Set<E> mergeCopy(Set<E>... 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 <E> the set element type
* Adds all elements from multiple sets into a target set.
*
* <p>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.
*
* <p><b>Example:</b>
* <pre>{@code
* Set<String> existing = new HashSet<>();
* existing.add("apple");
* SetUtil.addAll(existing, Set.of("banana"), Set.of("cherry"));
* // existing now contains: {apple, banana, cherry}
* }</pre>
*
* @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 <E> the element type
* @throws NullPointerException if target is null
*/
public static <E> Set<E> addAll(@NonNull Set<E> target, Set<E>... sets) {
for (Set<E> set : sets) {
Expand Down
Loading
Loading