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.rest.server.springboot;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.ThrowableUtils.*;
021
022import java.util.*;
023import java.util.concurrent.*;
024import java.util.function.*;
025
026import org.springframework.boot.context.properties.source.*;
027import org.springframework.core.env.*;
028
029/**
030 * Bridges a Spring {@link Environment} into the Juneau {@link org.apache.juneau.commons.settings.Settings Settings}
031 * singleton as a {@link org.apache.juneau.commons.settings.PropertySource PropertySource}.
032 *
033 * <p>
034 * Allows {@link org.apache.juneau.commons.inject.Value @Value("&#123;spring.property.key&#125;")} resolution
035 * against the same property sources that Spring Boot would resolve through
036 * {@code @org.springframework.beans.factory.annotation.Value}: {@code application.yaml},
037 * {@code application.properties}, command-line args, environment variables, and any custom
038 * {@code org.springframework.core.env.PropertySource} that Spring loads.
039 *
040 * <h5 class='section'>Lifecycle:</h5>
041 * <p>
042 * {@link SpringBeanStore} registers one instance per {@link org.springframework.context.ApplicationContext} in
043 * {@link org.apache.juneau.commons.settings.Settings Settings}, shared by every store on that context.  It is removed
044 * when every such store has been {@link SpringBeanStore#clear() cleared}, or when the context closes (for a
045 * {@link org.springframework.context.ConfigurableApplicationContext ConfigurableApplicationContext}).  Callers that
046 * want explicit lifecycle control can construct the source directly and add it via {@code Settings.get().addSource(...)};
047 * such instances are not shared.
048 *
049 * <h5 class='section'>Miss cache:</h5>
050 * <p>
051 * Juneau's builder keys (e.g. {@code MarshallingContext.unsortedProperties}) are not valid Spring Boot property names,
052 * so Spring walks every raw property source for each of them.  This source remembers up to 4096 such keys that were
053 * absent.  Every lookup checks the Environment's property-source list by identity (an added, removed or replaced
054 * source drops the cache) and re-checks {@link System#getProperty(String)} for a cached key.  Valid Boot names and
055 * {@code juneau.profiles.active} are never cached, and hits are never cached.  A source whose <em>contents</em> change
056 * in place (a mutated map, {@code MockEnvironment.setProperty}) is not detected for a key already cached as missing;
057 * call {@link #invalidate()} after such a change.  Setting {@code juneau.spring.missCache.disable=true} in the Spring
058 * Environment before the first lookup turns the cache off.
059 *
060 * <h5 class='section'>Lazy resolution:</h5>
061 * <p>
062 * The {@code Environment} is resolved lazily on the first {@link #get(String)} call so that
063 * constructors that build the source from an {@code ApplicationContext} do not interact with the
064 * context until a lookup actually happens.  This matters for tests that mock the context and assert
065 * "no interactions" until a Spring-side bean is requested.
066 *
067 * <h5 class='section'>Ordering:</h5>
068 * <p>
069 * Because {@code Settings} walks property sources in reverse insertion order, the Spring
070 * {@code Environment} bridge — registered when Spring creates the bean store — shadows any
071 * classpath-default {@code juneau.cfg} loaded earlier through the SPI-discovered
072 * {@code ConfigPropertySourceProvider}.  Programmatic {@code addSource()} calls made after the
073 * Spring context is built (e.g. from a user-provided microservice {@code Config}) still win.
074 *
075 * <h5 class='section'>See Also:</h5><ul>
076 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ValueAnnotation">@Value Annotation Basics</a>
077 *    <li class='jc'>{@link SpringBeanStore}
078 * </ul>
079 */
080public class SpringEnvironmentPropertySource implements org.apache.juneau.commons.settings.PropertySource {
081
082   /** Soft cap on cached misses per bridge; same policy as J1037's source cache (copied, not referenced). */
083   static final int MISS_CACHE_MAX_ENTRIES = 4096;
084
085   /** Opt-out switch, read once at first resolve from the bridged Environment itself (never through Settings). */
086   private static final String MISS_CACHE_DISABLE_KEY = "juneau.spring.missCache.disable";
087
088   private final Supplier<Environment> envSupplier;
089
090   // Written inside resolve() before resolved; readers read resolved first, so plain fields are safely published.
091   private volatile boolean resolved;
092   @SuppressWarnings({
093      "java:S3077" // Publish-once reference: env is assigned before resolved is set (constructor or resolve()) and is never compound-mutated, so volatile visibility is sufficient.
094   })
095   private volatile Environment env;
096   private boolean cacheEnabled;
097
098   // The miss set for the source list it was built from; replaced, never edited, when the list changes.
099   private volatile MissGeneration generation;
100
101   /**
102    * Constructor that bridges a pre-built {@link Environment}.
103    *
104    * @param env The Spring environment to bridge.  Must not be <jk>null</jk>.
105    */
106   public SpringEnvironmentPropertySource(Environment env) {
107      this.env = Objects.requireNonNull(env, "env");
108      this.envSupplier = null;
109   }
110
111   /**
112    * Constructor that lazily resolves the {@link Environment} on first lookup.
113    *
114    * <p>
115    * Use this overload when wiring through {@link org.springframework.context.ApplicationContext}: pass
116    * {@code appContext::getEnvironment} so the application context isn't invoked until a lookup
117    * actually occurs.
118    *
119    * @param envSupplier Supplier of the Spring environment.  Must not be <jk>null</jk>.  May return
120    *    <jk>null</jk>, in which case this source returns
121    *    {@link org.apache.juneau.commons.settings.PropertyLookupResult#missing()} for every key.
122    */
123   public SpringEnvironmentPropertySource(Supplier<Environment> envSupplier) {
124      this.envSupplier = Objects.requireNonNull(envSupplier, "envSupplier");
125   }
126
127   private synchronized void resolve() {
128      if (resolved)  // HTT: another thread resolved between the caller's check and lock acquisition; a single-threaded test cannot force it
129         return;
130      // safeOpt swallows exceptions thrown by the supplier, e.g. a mocked ApplicationContext that returns null or
131      // throws during getEnvironment().
132      var e = nn(envSupplier) ? safeOpt(envSupplier::get).orElse(null) : env;
133      env = e;
134      cacheEnabled = e instanceof ConfigurableEnvironment && ! safeOpt(() -> e.getProperty(MISS_CACHE_DISABLE_KEY)).map(Boolean::parseBoolean).orElse(false);
135      resolved = true;
136   }
137
138   @Override
139   public org.apache.juneau.commons.settings.PropertyLookupResult get(String name) {
140      if (! resolved)
141         resolve();
142      // Read env only after resolved: reading it first could see null, then resolved == true, and miss wrongly.
143      var e = env;
144      if (e == null)
145         return org.apache.juneau.commons.settings.PropertyLookupResult.missing();
146      // Profile piggyback: a Juneau config-profile activation lookup resolves to Spring's active profiles, so a
147      // Spring-Boot deployment has a single source of truth for which profiles are active.  Only answered when the
148      // caller has NOT explicitly set juneau.profiles.active as a property (that exact key wins if present).
149      if (eq(name, PROFILES_ACTIVE_KEY) && ! e.containsProperty(name)) {
150         var active = e.getActiveProfiles();
151         if (active.length > 0)  // getActiveProfiles() never returns null per the Environment contract.
152            return org.apache.juneau.commons.settings.PropertyLookupResult.present(o(String.join(",", active)));
153         return org.apache.juneau.commons.settings.PropertyLookupResult.missing();
154      }
155      var g = cacheEnabled && nn(name) ? currentGeneration((ConfigurableEnvironment)e) : null;
156      if (nn(g) && g.misses.contains(name)) {
157         // For an invalid Boot name, the raw systemProperties source (exact key) is the only one whose contents
158         // can change without a source-list change; systemEnvironment wraps System.getenv(), which cannot.
159         if (! g.hasSystemProperties || System.getProperty(name) == null)
160            return org.apache.juneau.commons.settings.PropertyLookupResult.missing();
161         g.misses.remove(name);
162      }
163      if (! e.containsProperty(name)) {
164         // Valid Boot names are never cached: Boot's relaxed binding can answer them from a changed source.
165         // isValid allocates, so it runs only here, on an uncached miss.
166         if (nn(g) && g.misses.size() < MISS_CACHE_MAX_ENTRIES && ! ConfigurationPropertyName.isValid(name))
167            g.misses.add(name);
168         return org.apache.juneau.commons.settings.PropertyLookupResult.missing();
169      }
170      // Spring's getProperty() returns null for a key mapped to an explicit null value, and throws
171      // IllegalArgumentException for an unresolvable nested placeholder.  o() maps the null to an empty value.
172      var v = e.getProperty(name);
173      return org.apache.juneau.commons.settings.PropertyLookupResult.present(o(v));
174   }
175
176   /**
177    * Clears this bridge's miss cache.
178    *
179    * <p>
180    * Needed only after a property source's contents change in place (for example a mutated
181    * {@code MapPropertySource} map, or {@code MockEnvironment.setProperty} in a test) for a key that is not a valid
182    * Spring Boot property name and that was already looked up and missed.  Adding, removing or replacing a property
183    * source, and setting a system property, are detected without it.  A lookup already in progress on another thread
184    * may still answer from the old cache; lookups that start after this method returns do not.
185    *
186    * @since 10.0.0
187    */
188   public void invalidate() {
189      generation = null;
190   }
191
192   /** @return The number of cached misses (tests). */
193   int missCount() {
194      var g = generation;
195      return g == null ? 0 : g.misses.size();
196   }
197
198   private MissGeneration currentGeneration(ConfigurableEnvironment e) {
199      var live = e.getPropertySources();
200      var g = generation;
201      if (nn(g) && g.matches(live))
202         return g;
203      // Racing threads may each build one; the loser's misses go to an unused generation, which only costs a walk.
204      g = new MissGeneration(live);
205      generation = g;
206      return g;
207   }
208
209   /**
210    * Cached misses for one snapshot of the Environment's property-source list.  A new generation replaces the old one
211    * when the list changes, so a miss computed against an old list can only land in a generation nobody reads.
212    */
213   private static final class MissGeneration {
214      final org.springframework.core.env.PropertySource<?>[] sources;
215      final boolean hasSystemProperties;
216      final Set<String> misses = ConcurrentHashMap.newKeySet();
217
218      MissGeneration(MutablePropertySources live) {
219         sources = live.stream().toArray(org.springframework.core.env.PropertySource<?>[]::new);
220         hasSystemProperties = Arrays.stream(sources).anyMatch(x -> StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME.equals(x.getName()));
221      }
222
223      // Identity, element by element, then count.  MutablePropertySources iterates its copy-on-write array without copying.
224      boolean matches(MutablePropertySources live) {
225         var i = 0;
226         for (var s : live) {
227            if (i >= sources.length || s != sources[i])
228               return false;
229            i++;
230         }
231         return i == sources.length;
232      }
233   }
234
235   /** The Juneau config-profile activation key that piggybacks on Spring's active profiles. */
236   private static final String PROFILES_ACTIVE_KEY = "juneau.profiles.active";
237}