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}