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→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}