All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.jstach.kiwi.kvs.KeyValues Maven / Gradle / Ivy

The newest version!
package io.jstach.kiwi.kvs;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.SequencedCollection;
import java.util.SequencedMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jspecify.annotations.Nullable;

import io.jstach.kiwi.kvs.interpolate.Interpolator;
import io.jstach.kiwi.kvs.interpolate.Interpolator.InterpolationException;
import io.jstach.kiwi.kvs.interpolate.Interpolator.MissingVariableInterpolationException;

/**
 * Represents a collection of {@link KeyValue} entries with various utility methods for
 * manipulation, transformation, and expansion. This interface serves as a central point
 * for managing key-value pairs, allowing operations such as filtering, mapping, and
 * interpolation.
 *
 * 

* KeyValues is basically a glorified {@code Supplier>}. If the key * values will be streamed multiple times it is recommended to call {@link #memoize()} * which will copy the key values if the key values are not already memoized. *

* * *

Key Features:

*
    *
  • Provides methods for building key-value pairs with {@link Builder}.
  • *
  • Supports functional transformations using {@link #map(UnaryOperator)}, * {@link #filter(Predicate)}, and {@link #flatMap(Function)}.
  • *
  • Allows interpolation and expansion of values using variable substitution.
  • *
  • Can convert key-values to a {@link Map} representation.
  • *
* *

Usage Example:

The following example demonstrates how to create, expand, and * transform key-values: * * {@snippet : * // Create a builder and add key-value pairs * KeyValues.Builder builder = KeyValues.builder(); * builder.add("key1", "value1"); * builder.add("key2", "${key1}-suffix"); * * // Build the KeyValues collection * KeyValues kvs = builder.build(); * * // Interpolate values using a Variables map * Variables variables = Variables.of("key1", "interpolated"); * KeyValues expanded = kvs.expand(variables); * * // Convert to a map * Map map = expanded.toMap(); * System.out.println(map); // {key1=interpolated, key2=interpolated-suffix} * } */ public interface KeyValues extends Iterable { /** * Returns a stream of the contained {@link KeyValue} entries. * @return a {@link Stream} of key-value pairs. */ public Stream stream(); /** * Creates a new {@link Builder} for constructing a {@code KeyValues} instance. * @param resource a {@link KeyValuesResource} defining the source URI and reference * key-value. * @return a new {@link Builder}. */ public static Builder builder(KeyValuesResource resource) { return new Builder(resource.uri(), resource.reference()); } /** * Creates a new {@link Builder} with a default, empty source. * @return a new {@link Builder}. */ public static Builder builder() { return new Builder(KeyValue.Source.NULL_URI, null); } /** * Returns an empty {@code KeyValues} instance. * @return an empty {@code KeyValues} collection. */ public static KeyValues empty() { return KeyValuesEmpty.EMPTY; } /** * Creates a {@code KeyValues} instance by copying the given collection. * @param kvs a {@link SequencedCollection} of key-value pairs. * @return a new {@code KeyValues} instance containing the provided key-values. */ public static KeyValues copyOf(SequencedCollection kvs) { return new ListKeyValues(List.copyOf(kvs)); } /** * Creates a single key values instance. * @param keyValue single key value. * @return keyvalues of one key value. */ public static KeyValues of(KeyValue keyValue) { return new ListKeyValues(List.of(keyValue)); } /** * A builder for constructing {@link KeyValues} instances from key-value pairs. * *

* The builder is designed to create key-value pairs with associated metadata, * including flags and source information. It supports adding entries both * individually and from collections, and allows setting flags for all added entries. *

* *

Usage Example:

The following example demonstrates how to use the builder: * * {@snippet : * // Create a new builder * KeyValues.Builder builder = KeyValues.builder(); * * // Add key-value pairs * builder.add("host", "localhost"); * builder.add("port", "8080"); * builder.flag(KeyValue.Flag.SENSITIVE); * * // Build the KeyValues instance * KeyValues kvs = builder.build(); * * // Convert to a map * Map map = kvs.toMap(); * System.out.println(map); // Output: {host=localhost, port=8080} * } */ public class Builder { private final URI source; private final @Nullable KeyValue reference; private final AtomicInteger index = new AtomicInteger(); private final List keyValues = new ArrayList<>(); private final EnumSet flags = EnumSet.noneOf(KeyValue.Flag.class); private final boolean noSource; /** * Constructs a new {@code Builder} with the specified source URI and reference. * @param source the URI representing the source of these key-values. * @param reference an optional reference {@link KeyValue}, can be {@code null}. */ private Builder(URI source, @Nullable KeyValue reference) { super(); this.source = source; this.reference = reference; this.noSource = source.equals(KeyValue.Source.NULL_URI); } /** * Adds a flag that will be applied to all key-value pairs added by this builder. * @param flag the {@link KeyValue.Flag} to add. * @return this builder, for method chaining. */ public Builder flag(KeyValue.Flag flag) { flags.add(flag); return this; } /** * Adds a key-value pair to the builder. * @param key the key as a {@link String}. * @param value the value as a {@link String}. * @return this builder, for method chaining. */ public Builder add(String key, String value) { keyValues.add(build(key, value)); return this; } /** * Adds a key-value pair to the builder from a map entry. * @param e a {@link java.util.Map.Entry} representing the key and value. * @return this builder, for method chaining. */ public Builder add(Entry e) { return add(e.getKey(), e.getValue()); } private static final Comparator> entryComparator = Map.Entry .comparingByKey() .thenComparing(Entry::getValue); /** * Adds a collection of entries to the builder. The entries are added in the order * they appear in the collection. * @param entries a {@link SequencedCollection} of entries to add. * @return this builder, for method chaining. */ public Builder add(SequencedCollection> entries) { for (var e : entries) { add(e); } return this; } /** * Adds a collection of entries to the builder. * *

* If the collection is a {@link SequencedCollection}, the entries are added in * the original order of the collection. Otherwise, the entries are first sorted * lexicographically by key and then by value before being added. *

* @param entries a {@link Collection} of entries to add. * @return this builder, for method chaining. */ public Builder add(Collection> entries) { switch (entries) { case SequencedCollection> sc -> add(sc); default -> entries.stream().sorted(entryComparator).forEach(this::add); } ; return this; } /** * Constructs a {@link KeyValue} with the given key and value, including flags and * source information. * @param key the key as a {@link String}. * @param value the value as a {@link String}. * @return a new {@link KeyValue} with the specified key, value, and metadata. */ public KeyValue build(String key, String value) { var s = noSource ? KeyValue.Source.EMPTY : new KeyValue.Source(source, reference, index.incrementAndGet()); var m = KeyValue.Meta.of(key, value, s, flags); return new KeyValue(key, value, m); } /** * Builds and returns a {@link KeyValues} instance containing all the key-value * pairs added to this builder. * @return a new {@link KeyValues} instance. */ public KeyValues build() { return KeyValues.copyOf(keyValues); } } /** * Collects a stream of {@link KeyValue} into a {@code KeyValues} instance. * @return a {@link Collector} that accumulates key-values into a {@code KeyValues} * object. */ public static Collector collector() { return Collectors.collectingAndThen(Collectors.toList(), KeyValues::copyOf); } /** * Collects a stream of {@link java.util.Map.Entry} into a {@code KeyValues} using the * provided builder. * @param builder the {@link Builder} to use for constructing the key-values. * @return a {@link Collector} that accumulates entries into a {@code KeyValues} * object. */ public static Collector, ?, KeyValues> collector(Builder builder) { return Collector.of(() -> builder, // Accumulator: add each entry to the builder (b, entry) -> b.add(entry), // Combiner: combine two builders (can be ignored if the stream is // sequential) (b1, b2) -> { throw new UnsupportedOperationException("Parallel streams not supported"); }, // Finisher: call build on the final builder Builder::build); } @Override default Iterator iterator() { return stream().iterator(); } /** * Applies a transformation function to each key-value pair in the collection. * @param kv a {@link UnaryOperator} to transform each key-value. * @return a new {@code KeyValues} with transformed entries. */ default KeyValues map(UnaryOperator kv) { return ToStringableKeyValues.of(() -> stream().map(kv)); } /** * Filters the key-value pairs based on a predicate. * @param predicate a {@link Predicate} to test each key-value. * @return a new {@code KeyValues} with filtered entries. */ default KeyValues filter(Predicate predicate) { return ToStringableKeyValues.of(() -> stream().filter(predicate)); } /** * This method gets the last element of the key values stream. This ergonomic is * provided because later keys override earlier keys of the same name. * @return optional of the last key value if there is one. */ default Optional last() { KeyValue last = null; for (var kv : this) { last = kv; } return Optional.ofNullable(last); } /** * Flattens the key-values by applying a mapping function that returns another * {@code KeyValues}. * @param func a function that transforms a {@link KeyValue} into another * {@code KeyValues}. * @return a new flattened {@code KeyValues}. */ default KeyValues flatMap(Function func) { var f = func.andThen(KeyValues::stream); return ToStringableKeyValues.of(() -> stream().flatMap(f)); } /** * Interpolates the values using the provided {@link Variables} map. * @param variables a {@link Variables} map for value substitution. * @return a map of interpolated key-values. */ default Map interpolate(Variables variables) { return KeyValuesInterpolator.interpolateKeyValues(this, variables); } /** * Expands the key-values using variable interpolation. * @param variables a {@link Variables} map for value substitution. * @return a new {@code KeyValues} with expanded values. */ default KeyValues expand(Variables variables) { Map interpolated = interpolate(variables); return map(kv -> kv.withExpanded(interpolated::get)); } /** * Returns a memoized version of this {@code KeyValues} which means repeated calls to * iteratore or stream over the KeyValues will always generate the same result. * @return a memoized {@code KeyValues} instance. */ default KeyValues memoize() { if (this instanceof MemoizedKeyValues mkvs) { return mkvs; } return copyOf(stream().toList()); } /** * Redacts sensitive key-value entries by replacing their values. * @return a new {@code KeyValues} with redacted entries. */ default KeyValues redact() { return this.map(KeyValue::redact); } /** * Converts the key-values to a map where each key maps to its expanded value. The * returned Map is sequenced based on the order of the key-values. * @return a {@link Map} of key-value pairs. */ default SequencedMap toMap() { SequencedMap m = new LinkedHashMap<>(); stream().forEach(kv -> m.put(kv.key(), kv.expanded())); return m; } } interface ToStringableKeyValues extends KeyValues { static KeyValues of(KeyValues kvs) { if (kvs instanceof ToStringableKeyValues t) { return t; } return new PrintableKeyValues(kvs); } static String toString(KeyValues keyValues) { StringBuilder sb = new StringBuilder(); var kvs = keyValues.redact(); sb.append("KeyValues[").append("\n"); KeyValuesMedia.ofProperties().formatter().format(sb, kvs); sb.append("]").append("\n"); return sb.toString(); } } interface MemoizedKeyValues extends KeyValues { } record PrintableKeyValues(KeyValues values) implements ToStringableKeyValues { @Override public Stream stream() { return values.stream(); } @Override public String toString() { return ToStringableKeyValues.toString(this); } } record ListKeyValues(List keyValues) implements ToStringableKeyValues, MemoizedKeyValues { ListKeyValues { keyValues = List.copyOf(keyValues); } @Override public Stream stream() { return keyValues.stream(); } @Override public Iterator iterator() { return keyValues.iterator(); } @Override public KeyValues memoize() { return this; } @Override public Optional last() { if (keyValues.isEmpty()) { return Optional.empty(); } return Optional.of(keyValues.getLast()); } @Override public final String toString() { return ToStringableKeyValues.toString(this); } } class KeyValuesInterpolator { static Map interpolateKeyValues(final KeyValues keyValues, final Variables variables) { List kvs = keyValues.stream().toList(); final Map flat = new HashMap<>(kvs.size()); kvs.forEach(kv -> flat.put(kv.key(), kv)); final Map resolved = new LinkedHashMap<>(kvs.size()); Variables combined = Variables.builder().add(k -> { var f = flat.get(k); if (f != null) { return f.raw(); } return null; }).add(resolved).add(variables).build(); Interpolator sub = Interpolator.create((key) -> { return combined.getValue(key); }); for (KeyValue kv : kvs) { KeyValue last = flat.remove(kv.key()); String v = kv.raw(); String value; if (kv.isNoInterpolation()) { value = v; } else { try { value = (v.indexOf('$') != -1) ? sub.interpolate(kv.key(), v) : v; } catch (MissingVariableInterpolationException e) { throw e; } catch (InterpolationException e) { String r = resolved.get(kv.key()); if (r == null) { r = kv.expanded(); } value = r; } } resolved.put(kv.key(), value); if (last != null) { flat.put(kv.key(), last); } } return resolved; } } enum KeyValuesEmpty implements KeyValues, ToStringableKeyValues { EMPTY; @Override public Stream stream() { return Stream.empty(); } @Override public String toString() { return "KeyValues[]"; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy