001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.inject;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import org.apache.juneau.commons.reflect.*;
024import org.apache.juneau.commons.settings.*;
025
026/**
027 * Reusable, prefix-scoped whole-object config binder — usable with or without the
028 * {@link ConfigProperties @ConfigProperties} annotation.
029 *
030 * <p>
031 * For each non-static, non-synthetic, non-final field on the target, builds relaxed candidate keys of the form
032 * {@code prefix.fieldName} (via {@link RelaxedPropertySource#candidates(String)}), resolves the first candidate
033 * present in the given {@link Settings} chain, and converts it via {@link Settings#toType(String, Class)}. A key
034 * absent from every candidate leaves the field's initializer value untouched ("bind-only-present" semantics). The
035 * same bind-only-present rule governs a nested {@code @ConfigProperties} field that starts out <jk>null</jk>: the
036 * nested type is only materialized if at least one key resolves anywhere under its sub-prefix; otherwise the field
037 * is left <jk>null</jk>.
038 *
039 * <h5 class='section'>Usage</h5>
040 * <p class='bjava'>
041 *    MyServiceProperties <jv>props</jv> = ConfigPropertiesBinder.<jsm>of</jsm>(<jk>new</jk> MyServiceProperties(), <js>"MyService"</js>).run();
042 * </p>
043 *
044 * <h5 class='section'>Precedence (candidate-major)</h5>
045 * <p>
046 * Precedence is <b>candidate-major</b>: each spelling (in {@link RelaxedPropertySource#candidates(String)} order —
047 * verbatim, upper-underscore, lower-dotted) is resolved through the full {@link Settings} source chain before the
048 * next spelling is tried. This means a verbatim hit in a lower-precedence source can outrank a relaxed-spelling
049 * override set on a higher-precedence source, if that override used a different spelling than the one that hit
050 * first.
051 * </p>
052 *
053 * <h5 class='section'>Caller-scoped sources</h5>
054 * <p>
055 * When a {@link #beanStore(BeanStore)} is supplied, its {@link PropertySource}-typed beans are
056 * consulted <b>ahead of</b> the global {@link Settings} chain for each candidate key, but <b>after</b> any
057 * {@link Settings#isOverridden(String) local/global test override} recorded on {@link #settings(Settings)} — a
058 * {@code Settings.setLocal}/{@code setGlobal} override always wins over a scoped source, and among the scoped
059 * sources the first one that reports the key present wins. This exactly mirrors the {@code @Value} resolution path
060 * ({@link org.apache.juneau.commons.svl.vars.PropertyVar}), letting a caller layer overriding or supplementary
061 * sources without mutating the shared {@link Settings#get() singleton}. Absent any scoped {@code PropertySource}
062 * beans, resolution is identical to binding against {@link #settings(Settings)} alone.
063 * </p>
064 *
065 * <h5 class='section'>Limitation — raw-type conversion only</h5>
066 * <p>
067 * Unlike {@link Value @Value}, conversion here is keyed on the field's <b>raw</b> {@link Class} (via
068 * {@link FieldInfo#getFieldType()}), not its full generic {@link java.lang.reflect.Type}. Generic container types
069 * such as {@code Optional<T>} or {@code Supplier<T>} are therefore <b>not</b> supported as {@code @ConfigProperties}
070 * field types; use a plain field type instead.
071 * </p>
072 *
073 * <h5 class='section'>Nested fields and circular nesting</h5>
074 * <p>
075 * A field whose type is itself {@code @ConfigProperties}-annotated is bound recursively under
076 * {@code prefix.fieldName}. The set of nested types along the active recursion path is tracked, and re-entering a
077 * type already on that path throws a {@link RuntimeException} instead of recursing indefinitely. A nested type
078 * that needs to be materialized (see the bind-only-present rule above) must expose an accessible no-arg
079 * constructor; if it doesn't, materialization throws a {@link RuntimeException} naming the offending field.
080 * </p>
081 *
082 * <h5 class='section'>See Also:</h5><ul>
083 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ConfigProperties">@ConfigProperties Annotation Basics</a>
084 * </ul>
085 *
086 * @param <T> The target type.
087 * @since 10.0.0
088 */
089@SuppressWarnings({
090   "java:S107", // The private bind/bindField/bindNested/hasResolvableKey helpers thread the same binding context (target, prefix, settings, sources, flags, validator, path) through recursion
091   "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants
092})
093public final class ConfigPropertiesBinder<T> {
094
095   /**
096    * Creates a new binder for {@code target}'s fields under {@code prefix}.
097    *
098    * <p>
099    * Defaults (overridable via the fluent setters before calling {@link #run()}): {@link Settings#get()} for the
100    * settings chain, <jk>true</jk> for {@link #relaxed(boolean)}, and {@link ConfigPropertiesValidator#NO_OP} for
101    * {@link #validator(ConfigPropertiesValidator)}.
102    *
103    * @param <T> The target type.
104    * @param target The instance to populate. Must not be <jk>null</jk>.
105    * @param prefix The property-key prefix (no trailing dot). Must not be <jk>null</jk>.
106    * @return A new binder builder.
107    */
108   public static <T> ConfigPropertiesBinder<T> of(T target, String prefix) {
109      return new ConfigPropertiesBinder<>(target, prefix);
110   }
111
112   private final T target;
113   private final String prefix;
114   private Settings settings = Settings.get();
115   private boolean relaxed = true;
116   private ConfigPropertiesValidator validator = ConfigPropertiesValidator.NO_OP;
117   private BeanStore beanStore;
118
119   private ConfigPropertiesBinder(T target, String prefix) {
120      this.target = reqnn("target", target);
121      this.prefix = reqnn("prefix", prefix);
122   }
123
124   /**
125    * Sets the resolver chain to bind against (overridable for tests).
126    *
127    * @param value The settings chain. Must not be <jk>null</jk> when {@link #run()} is called.
128    * @return This object.
129    */
130   public ConfigPropertiesBinder<T> settings(Settings value) {
131      settings = value;
132      return this;
133   }
134
135   /**
136    * Sets whether relaxed matching is enabled.
137    *
138    * @param value When <jk>true</jk>, tries {@link RelaxedPropertySource#candidates(String)} variants on a verbatim miss. Defaults to <jk>true</jk>.
139    * @return This object.
140    */
141   public ConfigPropertiesBinder<T> relaxed(boolean value) {
142      relaxed = value;
143      return this;
144   }
145
146   /**
147    * Sets the post-bind validation hook invoked once on each fully-bound target.
148    *
149    * @param value The post-bind hook. Must not be <jk>null</jk> when {@link #run()} is called. Defaults to {@link ConfigPropertiesValidator#NO_OP}.
150    * @return This object.
151    */
152   public ConfigPropertiesBinder<T> validator(ConfigPropertiesValidator value) {
153      validator = value;
154      return this;
155   }
156
157   /**
158    * Sets a caller-scoped {@link BeanStore} whose {@link PropertySource}-typed beans are consulted
159    * <b>ahead of</b> the global {@link Settings} chain when resolving each candidate key.
160    *
161    * <p>
162    * This mirrors the way {@code @Value} resolution layers a store's {@code PropertySource} beans over the
163    * global chain: {@link BeanStore#getBeansOfType(Class) beanStore.getBeansOfType(PropertySource.class)} walks
164    * parent&rarr;local, so a caller (e.g. a per-{@code RestContext} config bridge) can supply overriding or
165    * supplementary property sources without mutating the shared {@link Settings#get() singleton}. When the store
166    * has no {@code PropertySource} beans, behavior is identical to binding against {@link #settings(Settings)}
167    * alone (no allocation, no behavior change).
168    *
169    * @param value The caller-scoped bean store, or <jk>null</jk> for none (the default).
170    * @return This object.
171    */
172   public ConfigPropertiesBinder<T> beanStore(BeanStore value) {
173      beanStore = value;
174      return this;
175   }
176
177   /**
178    * Runs the bind against {@code target}.
179    *
180    * @return The same target instance (for method chaining).
181    * @throws IllegalArgumentException If {@link #settings(Settings)} or {@link #validator(ConfigPropertiesValidator)}
182    *    was set to <jk>null</jk>.
183    * @throws RuntimeException If circular {@code @ConfigProperties} nesting is detected, a property value could not
184    *    be converted to its field's type, or a nested {@code @ConfigProperties} type could not be instantiated.
185    */
186   public T run() {
187      reqnn("settings", settings);
188      reqnn("validator", validator);
189      var scopedSources = scopedSources(beanStore);
190      var path = new HashSet<Class<?>>();
191      path.add(target.getClass());
192      bind(target, prefix, settings, scopedSources, relaxed, validator, path);
193      return target;
194   }
195
196   private static void bind(Object target, String prefix, Settings settings, PropertySource[] scopedSources, boolean relaxed, ConfigPropertiesValidator validator, Set<Class<?>> path) {
197      var ci = ClassInfo.of(target);
198      for (var field : ci.getAllFields()) {
199         if (isBindable(field))
200            bindField(target, prefix, field, settings, scopedSources, relaxed, validator, path);
201      }
202      validator.validate(target);
203   }
204
205   private static void bindField(Object target, String prefix, FieldInfo field, Settings settings, PropertySource[] scopedSources, boolean relaxed, ConfigPropertiesValidator validator, Set<Class<?>> path) {
206      var key = prefix + "." + field.getName();
207      var fieldType = field.getFieldType();
208      if (fieldType.hasAnnotation(ConfigProperties.class)) {
209         bindNested(target, key, field, fieldType, settings, scopedSources, relaxed, validator, path);
210         return;
211      }
212      for (var candidate : candidateKeys(key, relaxed)) {
213         var value = resolve(candidate, settings, scopedSources);
214         if (value.isPresent()) {
215            try {
216               // Settings.toType only resolves wrapper types (e.g. Boolean, Integer) via reflection,
217               // not raw primitive Class objects, so primitive fields must be unwrapped first.
218               field.set(target, settings.toType(value.get(), fieldType.getWrapperIfPrimitive().inner()));
219            } catch (RuntimeException e) {
220               throw rex(e, "Could not bind property ''%s'' to field %s", candidate, field.getLabel());
221            }
222            return;
223         }
224      }
225      // Bind-only-present: no candidate resolved anywhere in the chain, field initializer default stands.
226   }
227
228   private static void bindNested(Object target, String key, FieldInfo field, ClassInfo fieldType, Settings settings, PropertySource[] scopedSources, boolean relaxed, ConfigPropertiesValidator validator, Set<Class<?>> path) {
229      if (! path.add(fieldType.inner()))
230         throw rex("Circular @ConfigProperties nesting detected at %s", field.getLabel());
231      try {
232         var nested = field.get(target);
233         if (nested == null) {
234            // No-materialize contract: only instantiate the nested type if binding it would actually set something.
235            if (! hasResolvableKey(fieldType, key, settings, scopedSources, relaxed, path))
236               return;
237            try {
238               nested = fieldType.newInstance();
239            } catch (RuntimeException e) {
240               throw rex(e, "Could not instantiate nested @ConfigProperties field %s", field.getLabel());
241            }
242            bind(nested, key, settings, scopedSources, relaxed, validator, path);
243            field.set(target, nested); // Materialize-if-absent: only assign when we created a new instance.
244         } else {
245            bind(nested, key, settings, scopedSources, relaxed, validator, path);
246         }
247      } finally {
248         path.remove(fieldType.inner());
249      }
250   }
251
252   /**
253    * Probes whether at least one bindable field under {@code type} — recursively through further nested
254    * {@code @ConfigProperties} fields — resolves to a present key under {@code prefix}, without binding
255    * anything. Used to decide whether a <jk>null</jk> nested field should be materialized.
256    */
257   @SuppressWarnings({
258      "java:S3776" // Cognitive complexity from mirroring bind()'s recursive nested-field walk (without side effects) is inherent to the probe contract; standing project policy is to suppress rather than refactor for this metric alone.
259   })
260   private static boolean hasResolvableKey(ClassInfo type, String prefix, Settings settings, PropertySource[] scopedSources, boolean relaxed, Set<Class<?>> path) {
261      for (var field : type.getAllFields()) {
262         if (isBindable(field)) {
263            var key = prefix + "." + field.getName();
264            var fieldType = field.getFieldType();
265            if (fieldType.hasAnnotation(ConfigProperties.class)) {
266               if (! path.add(fieldType.inner()))
267                  continue; // Already on the active path; the real bind() pass raises the circular-nesting error if this branch is ever taken.
268               try {
269                  if (hasResolvableKey(fieldType, key, settings, scopedSources, relaxed, path))
270                     return true;
271               } finally {
272                  path.remove(fieldType.inner());
273               }
274            } else {
275               for (var candidate : candidateKeys(key, relaxed))
276                  if (resolve(candidate, settings, scopedSources).isPresent())
277                     return true;
278            }
279         }
280      }
281      return false;
282   }
283
284   /**
285    * A field is bindable if it's not static/final/synthetic and is not already owned by {@code @Value}/{@code @Inject}.
286    *
287    * <p>
288    * Skipping {@code @Value}/{@code @Inject} fields keeps whole-object config binding from silently overwriting a
289    * value already resolved by the injection engine (e.g. a {@code prefix.fieldName} key must not clobber a field
290    * that {@code @Value} owns).  {@code @Value}/{@code @Inject} field resolution is handled separately by
291    * {@link ClassInfo#inject(Object, BeanStore, Runnable)} before this binder runs.
292    */
293   private static boolean isBindable(FieldInfo field) {
294      return field.isNotFinal() && field.isNotStatic() && ! field.isSynthetic() && ! isValueOrInjectField(field);
295   }
296
297   private static boolean isValueOrInjectField(FieldInfo field) {
298      return field.getAnnotations().stream().anyMatch(a -> JsrSupport.isValueAnnotation(a) || JsrSupport.isInjectAnnotation(a));
299   }
300
301   /**
302    * Resolves a single candidate key. Honors a {@link Settings#isOverridden(String) local/global test override} on
303    * {@code settings} first — matching {@link org.apache.juneau.commons.svl.vars.PropertyVar}'s
304    * override-before-scoped-sources contract, a {@code Settings.setLocal}/{@code setGlobal} override always wins
305    * over a scoped source — then consults the caller-scoped {@link PropertySource}s (if any) ahead of the global
306    * {@link Settings} chain, the first scoped source that reports the key present wins. Falls through to
307    * {@link Settings#get(String)} when neither an override nor a scoped source carries the key.
308    */
309   private static Optional<String> resolve(String candidate, Settings settings, PropertySource[] scopedSources) {
310      if (settings.isOverridden(candidate))
311         return settings.getOverride(candidate);
312      if (scopedSources != null) {
313         for (var source : scopedSources) {
314            if (source == null)
315               continue; // getBeansOfType can yield a null element from a supplier returning null.
316            var r = source.get(candidate);
317            if (r.isPresent())
318               return r.value();
319         }
320      }
321      return settings.get(candidate).asOptional();
322   }
323
324   /**
325    * Returns the caller-scoped {@link PropertySource} array for the supplied {@link BeanStore}, or {@code null} if
326    * the store is <jk>null</jk> or carries no {@code PropertySource}-typed beans (matching the "no behavior change
327    * when no scoped sources are present" contract).  Iteration order is that of
328    * {@link BeanStore#getBeansOfType(Class)} (highest-priority scoped source first).
329    */
330   @SuppressWarnings({
331      "java:S1168" // Null is a meaningful "no scoped sources" signal that resolve() branches on to preserve the no-allocation / no-behavior-change contract; an empty array would be equivalent but wasteful.
332   })
333   private static PropertySource[] scopedSources(BeanStore beanStore) {
334      if (beanStore == null)
335         return null;
336      var map = beanStore.getBeansOfType(PropertySource.class);
337      if (map.isEmpty())
338         return null;
339      return map.values().toArray(new PropertySource[0]);
340   }
341
342   private static List<String> candidateKeys(String key, boolean relaxed) {
343      return relaxed ? RelaxedPropertySource.candidates(key) : List.of(key);
344   }
345}