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}