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.config; 018 019import static org.apache.juneau.commons.utils.ObjectUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import org.apache.juneau.commons.settings.*; 023 024/** 025 * Auto-registers a classpath-default {@code juneau.cfg} (and the other names returned by 026 * {@link Config#getCandidateSystemDefaultConfigNames()}) as a {@link PropertySource} for the 027 * process-wide {@link Settings} singleton. 028 * 029 * <p> 030 * Discovered by {@link org.apache.juneau.commons.settings.Settings.Builder#useServiceLoader() ServiceLoader-based wiring} through a 031 * {@code META-INF/services/org.apache.juneau.commons.settings.PropertySourceProvider} entry 032 * shipped in this module. 033 * 034 * <p> 035 * Lookup semantics: the registered source defers calling {@link Config#getSystemDefault()} to the 036 * first {@code get(name)} invocation. This is deliberate — calling {@code getSystemDefault()} from 037 * inside {@link #create()} would create a static-initialization cycle because 038 * {@code Settings.<clinit>} fires the ServiceLoader at class-load time, and {@code Config}'s 039 * static initializers transitively reference {@link Settings} (through {@code Cache} / {@code SystemUtils.env(...)}) 040 * which would re-enter the {@code Settings.<clinit>} block. 041 * 042 * <p> 043 * Lazy resolution makes the cycle a non-issue: by the time {@code @Value("${...}")} fires its first 044 * lookup, all participating classes are fully initialized. 045 * 046 * <h5 class='section'>Silence on missing config:</h5> 047 * <p> 048 * If no system-default {@link Config} is discoverable (no {@code juneau.cfg} / {@code default.cfg} 049 * / {@code application.cfg} / etc. on the classpath or working directory), {@code get(name)} 050 * returns {@link PropertyLookupResult#missing()} for every key. No logging, no exception — many 051 * deployments have no classpath config and shouldn't see noise. 052 * 053 * <h5 class='section'>Ordering:</h5> 054 * <p> 055 * {@link #order()} returns {@code 100}, which is "low" in the {@link PropertySourceProvider} 056 * scheme — provider-discovered classpath defaults register near the bottom of the source list so 057 * that programmatic {@code Settings.get().addSource(...)} calls (e.g. per-microservice config or 058 * the Spring {@code Environment} bridge) shadow the classpath default. {@link Settings} walks 059 * sources in reverse insertion order, so the most-recently-added source wins. 060 */ 061public class ConfigPropertySourceProvider implements PropertySourceProvider { 062 063 @Override 064 public PropertySource create() { 065 // Return a lazy wrapper. Resolving Config.getSystemDefault() eagerly here would trigger 066 // Config.<clinit>, which can transitively re-enter Settings.<clinit> through Cache / 067 // SystemUtils.env(...) — the SPI registration is invoked from inside the Settings static init 068 // block, so any cycle here surfaces as ExceptionInInitializerError. 069 return new LazyConfigPropertySource(); 070 } 071 072 @Override 073 public int order() { 074 // Low ordering: classpath defaults register first, so any programmatic 075 // addSource(...) added afterwards shadows the classpath defaults at lookup time 076 // (Settings walks sources in reverse insertion order). 077 return 100; 078 } 079 080 /** 081 * Defers {@link Config#getSystemDefault()} resolution to the first {@link #get(String)} call to 082 * avoid the static-initialization cycle described in the enclosing class' Javadoc. 083 * 084 * <p> 085 * Additionally protects against three runtime cycles at first-use time: 086 * <ol> 087 * <li><b>Bootstrap-static-init cycle (poisoning).</b> If this source is called from inside 088 * <i>any</i> JVM static-initializer chain that transitively loads {@code Config}, the 089 * downstream {@code Config.findSystemDefault()} → {@code FileStore.<clinit>} → 090 * {@code Context.Builder.build()} chain can fail with {@link NullPointerException} 091 * because {@code Context.<clinit>} hasn't finished yet. That failure <i>poisons</i> 092 * {@code FileStore} (every subsequent reference raises {@link NoClassDefFoundError}). 093 * To prevent the poisoning, the source detects when it's called from a static-initializer 094 * frame by walking the current stack for a {@code "<clinit>"} method name and short-circuits 095 * to {@link PropertyLookupResult#missing()} <i>without</i> memoizing — so the next call, 096 * from regular code, can resolve normally. 097 * <li><b>Reentrant resolution cycle.</b> {@link Config#findSystemDefault()} looks up the 098 * {@code "juneau.configFile"} system property via {@link Settings#get(String)}, which 099 * walks every registered source — including this one. Without a guard, that re-enters 100 * {@code Config.getSystemDefault()} on the same thread, which re-enters 101 * {@code findSystemDefault()}, which re-enters {@code Settings.get("juneau.configFile")}, 102 * ad infinitum (a {@link StackOverflowError}). A {@link ThreadLocal} sentinel short-circuits 103 * any re-entrant lookup on the same thread to {@code missing()}. 104 * <li><b>Defensive throwable catch.</b> Any unexpected throwable (e.g. a different class 105 * loader's {@code Config} init failure that the {@code <clinit>}-sniff missed) is 106 * swallowed and treated as "not yet available" — {@code get(name)} returns 107 * {@link PropertyLookupResult#missing()} without memoizing the failure. 108 * </ol> 109 */ 110 @SuppressWarnings({ 111 "java:S1181" // Intentional broad Throwable catch — see Javadoc. 112 }) 113 static final class LazyConfigPropertySource implements PropertySource { 114 115 // Thread-local re-entrance guard. Static so it covers both the lazy-init path and any 116 // concurrent lookup that may also reach a Config.getSystemDefault() call (defensively). 117 private static final ThreadLocal<Boolean> RESOLVING = ThreadLocal.withInitial(() -> Boolean.FALSE); 118 119 // Guarded by the synchronized block in get(...); volatile reads cover the fast-path check. 120 private volatile boolean resolved; 121 @SuppressWarnings({ 122 "java:S3077" // Publish-once reference: delegate is fully constructed before its single assignment inside the synchronized block, so volatile visibility is sufficient. 123 }) 124 private volatile ConfigPropertySource delegate; 125 126 @Override 127 @SuppressWarnings({ 128 "java:S3776" // Cognitive complexity acceptable for lazy config property source resolution 129 }) 130 public PropertyLookupResult get(String name) { 131 132 // Re-entrant guard: Config.findSystemDefault() walks Settings.get("juneau.configFile"), 133 // which round-trips through every source including this one. Short-circuit on 134 // re-entry so the outer Config.getSystemDefault() can make progress. 135 if (isTrue(RESOLVING.get())) 136 return PropertyLookupResult.missing(); 137 138 // Bootstrap guard: if we're being called from inside a <clinit> frame (e.g. another 139 // class's static-initializer chain transitively invokes Settings.get(...) → this 140 // source), don't attempt to resolve Config — doing so would force Config / FileStore / 141 // Context class loading from inside an already-in-progress class init, which poisons 142 // FileStore.<clinit> with a NoClassDefFoundError that survives the rest of the JVM 143 // lifetime. Short-circuit without memoizing; the next non-<clinit> call resolves. 144 if (! resolved && isInClinit()) 145 return PropertyLookupResult.missing(); 146 147 if (! resolved) { 148 synchronized (this) { 149 if (! resolved) { 150 // Re-check the <clinit> guard inside the lock — another thread may have 151 // flipped resolved between the first check and the lock acquire. 152 if (isInClinit()) 153 return PropertyLookupResult.missing(); 154 155 Config cfg; 156 RESOLVING.set(Boolean.TRUE); 157 try { 158 cfg = Config.getSystemDefault(); 159 } catch (@SuppressWarnings("unused") Throwable t) { 160 // Defense-in-depth: a Config-side init failure the <clinit> sniff 161 // missed. Return missing() without memoizing so the next lookup can 162 // retry once Config init has completed. 163 return PropertyLookupResult.missing(); 164 } finally { 165 RESOLVING.remove(); 166 } 167 // Silent when no juneau.cfg / default.cfg / etc. is discoverable — the 168 // SPI consumer sees missing() for every key but no log noise. 169 delegate = (cfg == null) ? null : new ConfigPropertySource(cfg); 170 resolved = true; 171 } 172 } 173 } 174 return delegate == null ? PropertyLookupResult.missing() : delegate.get(name); 175 } 176 177 /** 178 * Returns <jk>true</jk> if any frame in the current thread's call stack is a Java 179 * static-initializer ({@code "<clinit>"}). Used to detect — and avoid — the bootstrap-time 180 * cycle that would otherwise poison {@code FileStore.<clinit>}. 181 * 182 * <p> 183 * Cost: a single {@link Thread#getStackTrace()} call on the first lookup per source-thread 184 * pair. {@code resolved} is volatile so subsequent calls skip the stack walk entirely. 185 * 186 * @return <jk>true</jk> if the current call is being made from inside a {@code <clinit>} 187 * frame anywhere in the stack. 188 */ 189 private static boolean isInClinit() { 190 var trace = Thread.currentThread().getStackTrace(); 191 for (var frame : trace) { 192 if (eq(frame.getMethodName(), "<clinit>")) 193 return true; 194 } 195 return false; 196 } 197 } 198}