001package io.jstach.ezkv.kvs;
002
003import java.io.FileNotFoundException;
004import java.io.IOException;
005import java.io.UncheckedIOException;
006import java.net.URI;
007import java.nio.file.NoSuchFileException;
008import java.util.ArrayList;
009import java.util.List;
010import java.util.function.Consumer;
011import java.util.function.Function;
012
013/**
014 * Represents a loader responsible for loading {@link KeyValues} from configured sources.
015 * This interface defines the contract for loading key-value pairs, potentially involving
016 * interpolation or resource chaining.
017 *
018 * <p>
019 * Implementations of this interface may load key-values from various sources such as
020 * files, classpath resources, or system properties.
021 *
022 * @see KeyValues
023 * @see Variables
024 */
025public interface KeyValuesLoader {
026
027        /**
028         * Loads key-values from configured sources.
029         * @return a {@link KeyValues} instance containing the loaded key-value pairs
030         * @throws IOException if an I/O error occurs during loading
031         * @throws FileNotFoundException if a specified resource is not found (old io)
032         * @throws NoSuchFileException if a specified resource is not found (nio)
033         * @throws KeyValuesException if an error occurs while processing keys such as
034         * interpolation or invalid resource keys.
035         * @throws UncheckedIOException if an IO error happens that had to be wrapped. Some
036         * loaders may throw an unchecked IO because of the difficulty of checked exceptions.
037         * The KeyValues system will unwrap the exception and check if it is one of the
038         * previous missing resource exceptions.
039         */
040        public KeyValues load()
041                        throws IOException, FileNotFoundException, NoSuchFileException, KeyValuesException, UncheckedIOException;
042
043        /**
044         * A builder class for constructing instances of {@link KeyValuesLoader}. The builder
045         * allows adding multiple sources from which key-values will be loaded, as well as
046         * setting variables for interpolation. <strong>Note that the order of the "add" and
047         * {@link #variables} methods does matter.</strong>
048         *
049         * @apiNote The creation of the builder is currently encapsulated at the moment and is
050         * done by {@link KeyValuesSystem#loader}.
051         * @see KeyValuesSystem#loader()
052         */
053        public final class Builder implements KeyValuesLoader {
054
055                final Function<Builder, KeyValuesLoader> loaderFactory;
056
057                final List<Function<KeyValuesEnvironment, ? extends NamedKeyValuesSource>> sources = new ArrayList<>();
058
059                final List<Function<KeyValuesEnvironment, ? extends Variables>> variables = new ArrayList<>();
060
061                private int resourceCount = 0;
062
063                private String namePrefix = "root";
064
065                Builder(Function<Builder, KeyValuesLoader> loaderFactory) {
066                        super();
067                        this.loaderFactory = loaderFactory;
068                }
069
070                /**
071                 * Adds a {@link KeyValuesResource} as a source to the loader.
072                 * @param resource the resource to add
073                 * @return this builder instance
074                 */
075                public Builder add(KeyValuesResource resource) {
076                        sources.add(e -> resource);
077                        resourceCount++;
078                        return this;
079                }
080
081                /**
082                 * Add resource using callback on builder.
083                 * @param uri uri of resource
084                 * @param builder builder to add additional properties.
085                 * @return this.
086                 */
087                public Builder add(String uri, Consumer<KeyValuesResource.Builder> builder) {
088                        var b = KeyValuesResource.builder(uri);
089                        builder.accept(b);
090                        return add(b.build());
091                }
092
093                /**
094                 * Adds a named {@link KeyValues} source to the loader.
095                 * @param name the name of the source
096                 * @param keyValues the key-values to add
097                 * @return this builder instance
098                 */
099                public Builder add(String name, KeyValues keyValues) {
100                        sources.add(e -> new NamedKeyValues(name, keyValues));
101                        return this;
102                }
103
104                /**
105                 * Adds a {@link URI} as a source by wrapping it in a {@link KeyValuesResource}.
106                 * The name of the resource will be automatically generated based on
107                 * {@link #namePrefix(String)} and a counter.
108                 * @param uri the URI to add
109                 * @return this builder instance
110                 */
111                public Builder add(URI uri) {
112                        return add(KeyValuesResource.builder(uri).name(namePrefix + resourceCount).build());
113                }
114
115                /**
116                 * Adds a URI specified as a string as a source to the loader. The name of the
117                 * resource will be automatically generated based on {@link #namePrefix(String)}
118                 * and a counter.
119                 * @param uri the URI string to add
120                 * @return this builder instance
121                 */
122                public Builder add(String uri) {
123                        return add(URI.create(uri));
124                }
125
126                /**
127                 * Adds {@link Variables} for interpolation when loading key-values.
128                 * @param variables the variables to use for interpolation
129                 * @return this builder instance
130                 */
131                public Builder add(Variables variables) {
132                        this.variables.add(e -> variables);
133                        return this;
134                }
135
136                /**
137                 * Adds variables that will be resolved based on the environment. <strong>
138                 * Variables resolution order is the opposite of KeyValues. Primacy takes
139                 * precedence! </strong> This is useful if you want to use environment things for
140                 * variables that are bound to {@link KeyValuesEnvironment}. This is preferred
141                 * instead of just creating Variables from {@link System#getProperties()} or
142                 * {@link System#getenv()} directly.
143                 * @param variablesFactory function to create variables from environment.
144                 * @return this
145                 * @see Variables#ofSystemProperties(KeyValuesEnvironment)
146                 * @see Variables#ofSystemEnv(KeyValuesEnvironment)
147                 * @see #add(Variables)
148                 */
149                public Builder variables(Function<KeyValuesEnvironment, Variables> variablesFactory) {
150                        this.variables.add(variablesFactory);
151                        return this;
152                }
153
154                /**
155                 * Adds resource that will be resolved based on the environment.
156                 * @param resourceFactory function to create resource from environment.
157                 * @return this
158                 * @see #add(KeyValuesResource)
159                 */
160                public Builder resource(Function<KeyValuesEnvironment, KeyValuesResource> resourceFactory) {
161                        this.sources.add(resourceFactory);
162                        return this;
163                }
164
165                /**
166                 * Sets the resource name prefix for auto naming of resources based on a counter.
167                 * This is to support the add methods that do not specify a resource name.
168                 * @param namePrefix must follow {@value KeyValuesResource#RESOURCE_NAME_REGEX}
169                 * @return by default <code>root</code>.
170                 */
171                public Builder namePrefix(String namePrefix) {
172                        this.namePrefix = KeyValuesSource.validateName(namePrefix);
173                        return this;
174                }
175
176                /**
177                 * Builds and returns a new {@link KeyValuesLoader} based on the current state of
178                 * the builder.
179                 *
180                 * <p>
181                 * If no sources are specified, a default classpath resource
182                 * {@code classpath:/system.properties} is used.
183                 * @return a new {@link KeyValuesLoader} instance
184                 */
185                public KeyValuesLoader build() {
186                        return loaderFactory.apply(this);
187                }
188
189                /**
190                 * Loads key-values using the current builder configuration.
191                 * @return a {@link KeyValues} instance containing the loaded key-value pairs
192                 * @throws IOException if an I/O error occurs during loading
193                 * @throws FileNotFoundException if a specified resource is not found. This
194                 * exception gets special treatment if thrown if the resource is optional it will
195                 * not be an error.
196                 * @throws NoSuchFileException if a specified resource is not found. This
197                 * exception gets special treatment if thrown if the resource is optional it will
198                 * not be an error.
199                 */
200                @Override
201                public KeyValues load() throws IOException, FileNotFoundException, NoSuchFileException {
202                        return build().load();
203                }
204
205        }
206
207}