001package io.jstach.ezkv.kvs;
002
003import java.net.URI;
004import java.util.Collection;
005import java.util.EnumSet;
006import java.util.Objects;
007import java.util.Set;
008import java.util.function.Function;
009
010import org.jspecify.annotations.Nullable;
011
012/**
013 * Represents a key-value pair in the Ezkv configuration system. Unlike a simple
014 * {@code Map.Entry<String, String>}, this record holds additional metadata that provides
015 * more context about the key-value pair, such as:
016 *
017 * <ul>
018 * <li>The original raw value (before any interpolation).</li>
019 * <li>The expanded value (after interpolation).</li>
020 * <li>Source information indicating where the key-value pair was loaded from.</li>
021 * <li>Flags indicating special behavior (e.g., sensitive data or disabling
022 * interpolation).</li>
023 * </ul>
024 *
025 * This class is immutable and provides methods for handling sensitive data,
026 * interpolation, and chaining resources for further key-value loading.
027 *
028 * <h2>Example Usage</h2>
029 *
030 * The following example shows how to load key-values using the Ezkv system:
031 *
032 * {@snippet :
033 *
034 * var kvs = KeyValuesSystem.defaults()
035 *      .loader()
036 *      .add("classpath:/start.properties")
037 *      .add("system:///")
038 *      .add("env:///")
039 *      .load();
040 *
041 * // Accessing key-value pairs:
042 * for (KeyValue kv : kvs) {
043 *      System.out.println("Key: " + kv.key() + ", Value: " + kv.value());
044 * }
045 * }
046 *
047 * @param key the key associated with this key-value pair.
048 * @param expanded the value after any interpolation.
049 * @param meta additional metadata associated with this key-value pair.
050 */
051public record KeyValue(String key, String expanded, Meta meta) {
052
053        /**
054         * Constructs a new {@code KeyValue} with the given key, expanded value, and
055         * associated metadata.
056         * @param key the key associated with this key-value pair (cannot be {@code null}).
057         * @param expanded the value after any interpolation (cannot be {@code null}).
058         * @param meta additional metadata (cannot be {@code null}).
059         */
060        public KeyValue {
061                Objects.requireNonNull(key);
062                Objects.requireNonNull(expanded);
063                Objects.requireNonNull(meta);
064        }
065
066        /**
067         * Constructs a new {@code KeyValue} with a raw value, using the provided key and raw
068         * value. The {@link Meta} information will be initialized with default settings.
069         * @param key the key associated with this key-value pair.
070         * @param raw the raw (unexpanded) value associated with this key.
071         */
072        public KeyValue(String key, String raw) {
073                this(key, raw, Meta.of(key, raw, Source.EMPTY, Set.of()));
074        }
075
076        public final static String REDACTED_MESSAGE = "REDACTED";
077
078        /**
079         * Gets the expanded value after interpolation.
080         * @return the expanded value.
081         */
082        public String value() {
083                return this.expanded;
084        }
085
086        /**
087         * Gets the original raw value before any interpolation.
088         * @return the raw value.
089         */
090        public String raw() {
091                return meta().raw();
092        }
093
094        /**
095         * Gets the set of flags associated with this key-value pair.
096         * @return the flags as an immutable set.
097         */
098        Set<Flag> flags() {
099                return meta().flags();
100        }
101
102        KeyValue replaceNullSource(URI uri) {
103                var original = this.meta.source();
104                if (!original.isNullResource()) {
105                        return this;
106                }
107                var source = original.withURI(uri);
108                var meta = this.meta.withSource(source);
109                return new KeyValue(key, expanded, meta);
110        }
111
112        /**
113         * Creates a new KeyValue with an updated key.
114         * @param key the new key.
115         * @return new key value.
116         */
117        public KeyValue withKey(String key) {
118                return new KeyValue(key, expanded, meta);
119        }
120
121        /**
122         * Creates a new {@code KeyValue} with an updated expanded value.
123         * @param expanded the new expanded value.
124         * @return a new {@code KeyValue} with the updated expanded value.
125         */
126        public KeyValue withExpanded(String expanded) {
127                return new KeyValue(key, expanded, meta);
128        }
129
130        /**
131         * Returns a new {@code KeyValue} instance with its value expanded using the provided
132         * function. The expansion function takes the key as input and returns the expanded
133         * value, or {@code null} if no expansion is necessary. If an expanded value is
134         * provided by the function, a new {@code KeyValue} instance is created with that
135         * expanded value; otherwise, the current instance is returned unchanged.
136         * @param expanded a function that takes the key as input and returns the expanded
137         * value or {@code null} if no expansion is needed
138         * @return a new {@code KeyValue} instance with the expanded value, or the original
139         * instance if no expansion was applied
140         */
141        public KeyValue withExpanded(@SuppressWarnings("exports") Function<String, @Nullable String> expanded) {
142                String n = key();
143                String exp = expanded.apply(n);
144                if (exp != null) {
145                        return withExpanded(exp);
146                }
147                return this;
148        }
149
150        /**
151         * Adds additional flags to this {@code KeyValue} and returns a new instance.
152         * @param flagsCol a collection of flags to add.
153         * @return a new {@code KeyValue} with the added flags.
154         */
155        public KeyValue addFlags(Collection<Flag> flagsCol) {
156                var flags = EnumSet.noneOf(Flag.class);
157                flags.addAll(flags());
158                flags.addAll(flagsCol);
159                var meta = this.meta.withFlags(flags);
160                return new KeyValue(key, expanded, meta);
161        }
162
163        /**
164         * Metadata associated with a {@link KeyValue}. This interface encapsulates additional
165         * information about a key-value pair, such as its raw, unexpanded value, its source,
166         * and any flags that modify its behavior.
167         *
168         * <h2>Components:</h2>
169         * <ul>
170         * <li>{@link #raw()} - The original, unexpanded value associated with the key.</li>
171         * <li>{@link #source()} - The source of the key-value, represented by a
172         * {@link KeyValue.Source}.</li>
173         * <li>{@link #flags()} - A set of {@link KeyValue.Flag} that can modify how the
174         * key-value is processed.</li>
175         * </ul>
176         *
177         * <h2>Immutability:</h2> Implementations of {@code Meta} are immutable, ensuring that
178         * once created, the metadata cannot be altered. However, methods are provided to
179         * create modified copies with updated flags or source information.
180         *
181         */
182        public sealed interface Meta {
183
184                /**
185                 * The original key that was collected on the source.
186                 * @return the original key.
187                 */
188                String originalKey();
189
190                /**
191                 * Retrieves the raw, unexpanded value associated with the {@link KeyValue}.
192                 * @return the original raw value as a {@link String}.
193                 */
194                String raw();
195
196                /**
197                 * Returns the source from which the key-value pair was loaded.
198                 * @return a {@link KeyValue.Source} representing the origin of the key-value.
199                 */
200                Source source();
201
202                /**
203                 * Retrieves any flags associated with the key-value. These flags can alter how
204                 * the key-value is interpreted or displayed.
205                 * @return a {@link Set} of {@link KeyValue.Flag} representing the flags.
206                 */
207                Set<Flag> flags();
208
209                /**
210                 * Creates a new {@code Meta} instance with the specified flags.
211                 * @param flags the collection of flags to include in the new metadata.
212                 * @return a new {@code Meta} instance with the updated flags.
213                 */
214                Meta withFlags(Collection<Flag> flags);
215
216                /**
217                 * Creates a new {@code Meta} instance with the specified source.
218                 * @param source the new source to associate with the metadata.
219                 * @return a new {@code Meta} instance with the updated source.
220                 */
221                Meta withSource(Source source);
222
223                /**
224                 * Factory method to create a new {@code Meta} instance with the specified raw
225                 * value, source, and flags.
226                 * @param originalKey original key.
227                 * @param raw the raw value associated with the key-value.
228                 * @param source the source from which the key-value was loaded.
229                 * @param flags the flags associated with the key-value.
230                 * @return a new {@code Meta} instance.
231                 */
232                static Meta of(String originalKey, String raw, Source source, Set<Flag> flags) {
233                        return new DefaultMeta(originalKey, raw, source, flags);
234                }
235
236        }
237
238        record DefaultMeta(String originalKey, String raw, Source source, Set<Flag> flags) implements Meta {
239                public DefaultMeta {
240                        flags = FlagSet.copyOf(flags, Flag.class);
241                }
242
243                @Override
244                public Meta withFlags(Collection<Flag> flags) {
245                        var flagsCopy = FlagSet.copyOf(flags, Flag.class);
246                        return new DefaultMeta(originalKey, raw, source, flagsCopy);
247                }
248
249                @Override
250                public Meta withSource(Source source) {
251                        return new DefaultMeta(originalKey, raw, source, flags);
252                }
253
254                @Override
255                public String toString() {
256                        return "Meta[raw=" + raw + ", source=" + Source.toString(source) + ", flags=" + flags + "]";
257                }
258
259        }
260
261        /**
262         * Represents the source information for a {@link KeyValue}. A {@code Source} can
263         * indicate where the key-value pair was loaded from, such as a configuration file,
264         * system properties, or environment variables. It also supports linking a key-value
265         * pair to another one through the {@code reference} field.
266         *
267         * <h2>Fields:</h2>
268         * <ul>
269         * <li>{@code uri} - The URI representing the origin of the key-value pair.</li>
270         * <li>{@code reference} - An optional {@link KeyValue} that this key-value depends on
271         * (e.g., for chained loading).</li>
272         * <li>{@code index} - A numerical identifier that can be used for ordering or
273         * tracking.</li>
274         * </ul>
275         *
276         * <h2>Usage Example:</h2>
277         *
278         * The following example demonstrates creating a source from a URI:
279         *
280         * {@snippet :
281         * URI fileUri = URI.create("file:///config.properties");
282         * KeyValue.Source source = new KeyValue.Source(fileUri, null, 0);
283         * System.out.println("Source URI: " + source.uri());
284         * }
285         *
286         * @param uri the URI representing the origin of the key-value pair.
287         * @param reference an optional key-value pair that this one references.
288         * @param index an optional index for ordering.
289         */
290        public record Source(URI uri, @Nullable KeyValue reference,
291                        int index) implements KeyValueReference, KeyValuesSource {
292
293                /**
294                 * A constant representing a "null" URI for cases where the source is not
295                 * specified.
296                 */
297                public static URI NULL_URI = URI.create("null:///");
298
299                /**
300                 * A default, empty source instance used when no specific source information is
301                 * available.
302                 */
303                public static Source EMPTY = new Source();
304
305                /**
306                 * Creates an empty source.
307                 * @see #EMPTY
308                 */
309                public Source() {
310                        this(NULL_URI, null, 0);
311                }
312
313                /**
314                 * Checks if this {@code Source} is considered a null resource.
315                 * @return {@code true} if the source URI is {@link #NULL_URI}, otherwise
316                 * {@code false}.
317                 */
318                boolean isNullResource() {
319                        return uri.equals(NULL_URI);
320                }
321
322                /**
323                 * Creates a new {@code Source} instance with the specified URI, keeping the other
324                 * fields the same.
325                 * @param uri the new URI to set.
326                 * @return a new {@code Source} with the updated URI.
327                 */
328                Source withURI(URI uri) {
329                        return new Source(uri, reference, index);
330                }
331
332                /**
333                 * Redacts the source.
334                 * @return redacted source.
335                 */
336                Source redact() {
337                        if (!KeyValueReference.isRedacted(this)) {
338                                return this;
339                        }
340                        var uri = URI.create(KeyValueReference.redactURI(this, KeyValue.REDACTED_MESSAGE));
341                        return new Source(uri, reference, index);
342                }
343
344                @Override
345                public String toString() {
346                        return toString(redact());
347                }
348
349                private static String toString(Source source) {
350                        if (source.isNullResource()) {
351                                return "Source[empty]";
352                        }
353                        return "Source[uri=" + source.uri
354                                        + (source.reference == null ? "" : ", reference=" + KeyValueReference.toStringRef(source.reference))
355                                        + ", index=" + source.index + "]";
356                }
357
358        }
359
360        /**
361         * Enumeration representing flags that can modify the behavior of a {@link KeyValue}.
362         * These flags provide additional metadata that can influence how key-value pairs are
363         * processed, stored, or displayed.
364         *
365         * <h2>Flags:</h2>
366         * <ul>
367         * <li>{@link #NO_INTERPOLATION} - Indicates that the key-value should not be
368         * interpolated, meaning its value should not undergo any variable substitution.</li>
369         * <li>{@link #SENSITIVE} - Marks the key-value as containing sensitive information,
370         * such as passwords or secrets, which may be redacted when displayed.</li>
371         * </ul>
372         */
373        public enum Flag {
374
375                /**
376                 * Indicates that the key-value pair should not undergo any interpolation or
377                 * variable substitution. Useful for cases where the raw value should be preserved
378                 * as-is.
379                 */
380                NO_INTERPOLATION,
381
382                /**
383                 * Marks the key-value as containing sensitive information. When this flag is set,
384                 * the value may be redacted when displayed to avoid leaking secrets.
385                 */
386                SENSITIVE;
387
388                /**
389                 * Checks if this flag is set for the given {@link KeyValue}.
390                 * @param kv the key-value pair to check.
391                 * @return {@code true} if the flag is set on the provided key-value, otherwise
392                 * {@code false}.
393                 */
394                public boolean isSet(KeyValue kv) {
395                        return kv.isFlag(NO_INTERPOLATION);
396                }
397
398        }
399
400        /**
401         * Checks if the {@link Flag#NO_INTERPOLATION} flag is set on this {@code KeyValue}.
402         * This indicates that the value should not undergo any interpolation or variable
403         * substitution.
404         * @return {@code true} if the {@code NO_INTERPOLATION} flag is set, otherwise
405         * {@code false}.
406         */
407        public boolean isNoInterpolation() {
408                return isFlag(Flag.NO_INTERPOLATION);
409        }
410
411        /**
412         * Checks if the {@link Flag#SENSITIVE} flag is set on this {@code KeyValue}. A
413         * sensitive value is marked to indicate that it should be treated as confidential and
414         * may be redacted when displayed to prevent leaking sensitive information.
415         * @return {@code true} if the {@code SENSITIVE} flag is set, otherwise {@code false}.
416         */
417        public boolean isSensitive() {
418                return isFlag(Flag.SENSITIVE);
419        }
420
421        /**
422         * Checks if a specific flag is set on this {@code KeyValue}.
423         * @param flag the flag to check
424         * @return {@code true} if the specified flag is set, otherwise {@code false}.
425         */
426        public boolean isFlag(Flag flag) {
427                return meta().flags().contains(flag);
428        }
429
430        /**
431         * Redacts the sensitive value in this key-value pair by replacing it with a default
432         * message.
433         * @return a new {@code KeyValue} with the sensitive data redacted.
434         */
435        public KeyValue redact() {
436                return redact(REDACTED_MESSAGE);
437        }
438
439        /**
440         * Redacts the sensitive value in this key-value pair by replacing it with a custom
441         * message.
442         * @param redactMessage the message to use for redaction.
443         * @return a new {@code KeyValue} with the sensitive data redacted.
444         */
445        public KeyValue redact(String redactMessage) {
446                if (isSensitive()) {
447                        String expanded = redactMessage;
448                        String raw = redactMessage;
449                        var flags = EnumSet.copyOf(this.meta.flags());
450                        flags.remove(Flag.SENSITIVE);
451                        var source = meta.source();
452                        var originalKey = meta.originalKey();
453                        Meta meta = new DefaultMeta(originalKey, raw, source, flags);
454                        return new KeyValue(key, expanded, meta);
455                }
456                return this;
457        }
458
459        boolean isOriginalKey() {
460                return key.equals(meta.originalKey());
461        }
462
463        @Override
464        public String toString() {
465                return toString(redact());
466        }
467
468        private static String toString(KeyValue kv) {
469                return "KeyValue[key='" + kv.key + "'" //
470                                + (kv.isOriginalKey() ? "" : ", originalKey='" + kv.meta().originalKey() + "'") //
471                                + ", raw='" + kv.raw() + "'" //
472                                + ", expanded='" + kv.expanded + "'"//
473                                + ", source=" + Source.toString(kv.meta().source())//
474                                + (kv.flags().isEmpty() ? "" : ", flags=" + kv.flags()) //
475                                + "]";
476        }
477
478}