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}