001package io.jstach.ezkv.kvs; 002 003import java.io.FileNotFoundException; 004import java.io.IOException; 005import java.io.InputStream; 006import java.lang.System.Logger.Level; 007import java.net.URI; 008import java.net.URL; 009import java.nio.file.FileSystem; 010import java.nio.file.FileSystems; 011import java.nio.file.Path; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.Collections; 015import java.util.List; 016import java.util.Map; 017import java.util.Properties; 018import java.util.Random; 019import java.util.stream.Stream; 020 021import org.jspecify.annotations.NonNull; 022import org.jspecify.annotations.Nullable; 023 024import io.jstach.ezkv.kvs.KeyValuesEnvironment.Logger; 025 026/** 027 * A facade over various system-level singletons used for loading key-value resources. 028 * This interface provides a flexible mechanism for accessing and overriding system-level 029 * components such as environment variables, system properties, and input streams. 030 * 031 * <p> 032 * Implementations can replace default system behaviors, enabling custom retrieval of 033 * environment variables or properties, or integrating custom logging mechanisms. 034 * 035 * @apiNote The API in this class uses traditional getter methods because the methods are 036 * often dynamic and to be consistent with the methods they are facading. 037 */ 038public interface KeyValuesEnvironment { 039 040 // TODO remove 041 /** 042 * If the loader builder is not passed any resources this resource will be used. 043 * @return default resource is <code>classpath:/boot.properties</code> 044 */ 045 default KeyValuesResource defaultResource() { 046 return KeyValuesResource.builder(URI.create("classpath:/boot.properties")).build(); 047 } 048 049 /** 050 * Retrieves the main method arguments. By default, returns an empty array. 051 * @return an array of main method arguments 052 */ 053 default @NonNull String[] getMainArgs() { 054 var logger = getLogger(); 055 logger.warn("Main Args were requested but not provided. Using fallback 'sun.java.command'"); 056 String value = getSystemProperties().getProperty("sun.java.command"); 057 if (value == null) { 058 throw new IllegalStateException("Cannot get main args from 'sun.java.command' as it was not provided"); 059 } 060 return fallbackMainArgs(value); 061 } 062 063 private static @NonNull String[] fallbackMainArgs(String sunJavaCommandValue) { 064 065 // Use fallback by extracting from system property 066 String command = sunJavaCommandValue; 067 if (!command.isEmpty()) { 068 // Split command into components (main class/jar is the first token) 069 List<String> components = new ArrayList<>(Arrays.asList(command.split("\\s+"))); 070 // Remove the first element (main class or jar) 071 if (!components.isEmpty()) { 072 components.remove(0); 073 } 074 // Return remaining as the main args 075 return components.toArray(new @NonNull String[0]); 076 } 077 078 // Return an empty array if no arguments are available 079 return new String[0]; 080 } 081 082 /** 083 * Retrieves the current system properties. By default, delegates to 084 * {@link System#getProperties()}. 085 * @return the current system properties 086 */ 087 default Properties getSystemProperties() { 088 return System.getProperties(); 089 } 090 091 /** 092 * Retrieves the current environment variables. By default, delegates to 093 * {@link System#getenv()}. 094 * @return a map of environment variables 095 */ 096 default Map<String, String> getSystemEnv() { 097 return System.getenv(); 098 } 099 100 /** 101 * Retrieves a random number generator. By default, returns a new {@link Random} 102 * instance. 103 * @return random. 104 */ 105 default Random getRandom() { 106 return new Random(); 107 } 108 109 /** 110 * Retrieves the standard input stream. By default, delegates to {@link System#in}. 111 * @return the standard input stream or an empty input stream if {@code System.in} is 112 * null 113 */ 114 default InputStream getStandardInput() { 115 InputStream i = System.in; 116 return i == null ? InputStream.nullInputStream() : i; 117 } 118 119 /** 120 * Retrieves the logger instance used for logging messages. By default, returns a noop 121 * logger. 122 * @return the logger instance 123 */ 124 default Logger getLogger() { 125 return Logger.of(); 126 } 127 128 /** 129 * Retrieves the {@link ResourceLoader} used for loading resources as streams. 130 * @return the resource stream loader instance 131 */ 132 default ResourceLoader getResourceLoader() { 133 return new ResourceLoader() { 134 135 @Override 136 public @Nullable InputStream getResourceAsStream(String path) throws IOException { 137 return getClassLoader().getResourceAsStream(path); 138 } 139 140 @Override 141 public Stream<URL> getResources(String path) throws IOException { 142 var cl = getClassLoader(); 143 return Collections.list(cl.getResources(path)).stream(); 144 } 145 }; 146 } 147 148 /** 149 * Retrieves the {@link FileSystem} used for loading file resources. 150 * @return by default {@link FileSystems#getDefault()}. 151 */ 152 default FileSystem getFileSystem() { 153 return FileSystems.getDefault(); 154 } 155 156 /** 157 * Gets the current working directory possibly using the passed in filesystem. 158 * @return cwd or {@code null} 159 */ 160 default @Nullable Path getCWD() { 161 return null; 162 } 163 164 /** 165 * Retrieves the class loader. By default, delegates to 166 * {@link ClassLoader#getSystemClassLoader()}. 167 * @return the system class loader 168 */ 169 default ClassLoader getClassLoader() { 170 return ClassLoader.getSystemClassLoader(); 171 } 172 173 /** 174 * Interface for loading resources. 175 */ 176 public interface ResourceLoader { 177 178 /** 179 * Retrieves an input stream for the specified resource path. 180 * @param path the path of the resource 181 * @return the input stream for the resource, or {@code null} if not found 182 * @throws IOException if an I/O error occurs 183 */ 184 public @Nullable InputStream getResourceAsStream(String path) throws IOException; 185 186 /** 187 * Retrieves classpath resources basically equilvant to 188 * {@link ClassLoader#getResources(String)}. 189 * @param path see {@link ClassLoader#getResources(String)}. 190 * @return a stream of urls. 191 * @throws IOException if an IO error happens getting the resources URLs. 192 */ 193 public Stream<URL> getResources(String path) throws IOException; 194 195 /** 196 * Opens an input stream for the specified resource path. Throws a 197 * {@link FileNotFoundException} if the resource is not found. 198 * @param path the path of the resource 199 * @return the input stream for the resource 200 * @throws IOException if an I/O error occurs 201 * @throws FileNotFoundException if the resource is not found 202 */ 203 default InputStream openStream(String path) throws IOException, FileNotFoundException { 204 InputStream s = getResourceAsStream(path); 205 if (s == null) { 206 throw new FileNotFoundException(path); 207 } 208 return s; 209 } 210 211 } 212 213 /** 214 * Key Values Resource focused logging facade and event capture. Logging level 215 * condition checking is purposely not supplied as these are more like events and many 216 * implementations will replay when the actual logging sytem loads. 217 */ 218 public interface Logger { 219 220 /** 221 * Returns a logger that uses the supplied {@link java.lang.System.Logger}. 222 * <em>Becareful using this because something downstream may need to configure the 223 * system logger based on Ezkv config.</em> 224 * @param logger system logger. 225 * @return logger. 226 */ 227 public static Logger of(System.Logger logger) { 228 return new SystemLogger(logger); 229 } 230 231 /** 232 * By default Ezkv does no logging because logging usually needs configuration 233 * loaded first (Ezkv in this case). 234 * @return noop logger. 235 */ 236 public static Logger of() { 237 return NoOpLogger.NOPLOGGER; 238 } 239 240 /** 241 * When Key Values System is loaded. 242 * @param system that was just created. 243 */ 244 default void init(KeyValuesSystem system) { 245 } 246 247 /** 248 * Logs a debug-level message. 249 * @param message the message to log 250 */ 251 public void debug(String message); 252 253 /** 254 * Logs an info-level message. 255 * @param message the message to log 256 */ 257 public void info(String message); 258 259 /** 260 * Logs an warn-level message. 261 * @param message the message to log 262 */ 263 public void warn(String message); 264 265 /** 266 * Logs a debug message indicating that a resource is being loaded. 267 * @param resource the resource being loaded 268 */ 269 default void load(KeyValuesResource resource) { 270 debug(KeyValueReference.describe(new StringBuilder("Loading "), resource, true).toString()); 271 } 272 273 /** 274 * Logs an info message indicating that a resource has been successfully loaded. 275 * @param resource the loaded resource 276 */ 277 default void loaded(KeyValuesResource resource) { 278 info(KeyValueReference.describe(new StringBuilder("Loaded "), resource, false).toString()); 279 } 280 281 /** 282 * Logs a debug message indicating that a resource is missing. 283 * @param resource the resource that was not found 284 * @param exception the exception that occurred when the resource was not found 285 */ 286 default void missing(KeyValuesResource resource, Exception exception) { 287 debug(KeyValueReference.describe(new StringBuilder("Missing "), resource, false).toString()); 288 } 289 290 /** 291 * This signals that key values system will no longer be used to load resources 292 * and that some other system can now take over perhaps the logging system. 293 * @param system key value system that was closed. 294 */ 295 default void closed(KeyValuesSystem system) { 296 } 297 298 /** 299 * This is to signal failure that the KeyValueSystem cannot recover from while 300 * attempting to load. 301 * @param exception unrecoverable key values exception 302 */ 303 default void fatal(Exception exception) { 304 305 } 306 307 /** 308 * Turns a Level into a SLF4J like level String that is all upper case and same 309 * length with right padding. {@link Level#ALL} is "<code>TRACE</code>", 310 * {@link Level#OFF} is "<code>ERROR</code>" and {@link Level#WARNING} is 311 * "<code>WARN</code>". 312 * @param level system logger level. 313 * @return upper case string of level. 314 */ 315 public static String formatLevel(Level level) { 316 return switch (level) { 317 case DEBUG -> /* */ "DEBUG"; 318 case ALL -> /* */ "TRACE"; 319 case ERROR -> /* */ "ERROR"; 320 case INFO -> /* */ "INFO "; 321 case OFF -> /* */ "ERROR"; 322 case TRACE -> /* */ "TRACE"; 323 case WARNING -> /* */ "WARN "; 324 }; 325 } 326 327 } 328 329} 330 331enum NoOpLogger implements Logger { 332 333 NOPLOGGER; 334 335 @Override 336 public void debug(String message) { 337 } 338 339 @Override 340 public void info(String message) { 341 } 342 343 @Override 344 public void warn(String message) { 345 } 346 347 @Override 348 public void load(KeyValuesResource resource) { 349 } 350 351 @Override 352 public void loaded(KeyValuesResource resource) { 353 } 354 355 @Override 356 public void missing(KeyValuesResource resource, Exception exception) { 357 } 358 359} 360 361final class SystemLogger implements Logger { 362 363 private final System.Logger logger; 364 365 SystemLogger(java.lang.System.Logger logger) { 366 super(); 367 this.logger = logger; 368 } 369 370 @Override 371 public void debug(String message) { 372 logger.log(Level.DEBUG, message); 373 } 374 375 @Override 376 public void info(String message) { 377 logger.log(Level.INFO, message); 378 } 379 380 @Override 381 public void warn(String message) { 382 logger.log(Level.WARNING, message); 383 384 } 385 386} 387 388@SuppressWarnings("ArrayRecordComponent") // TODO We will fix this later. 389record DefaultKeyValuesEnvironment(@NonNull String @Nullable [] mainArgs) implements KeyValuesEnvironment { 390 391 @Override 392 public @NonNull String[] getMainArgs() { 393 var args = mainArgs; 394 if (args == null) { 395 return KeyValuesEnvironment.super.getMainArgs(); 396 } 397 return args; 398 } 399 400}