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.settings;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.concurrent.*;
022
023/**
024 * Memoizing decorator for a {@link PropertySource} whose answers can never change after construction.
025 *
026 * <p>
027 * Every lookup result, hits and misses alike, is cached in a {@link ConcurrentHashMap}, so a repeated lookup costs one
028 * map read instead of whatever the delegate does (for example the relaxed environment source tokenizes the name and
029 * probes up to three environment variables).
030 *
031 * <h5 class='section'>Contract:</h5>
032 * <p>
033 * <b>Wrap only sources whose answers cannot change after construction, including their misses.</b>
034 * A wrapped source that answers {@link PropertyLookupResult#missing()} once, for any reason, answers it forever.
035 * Never wrap system properties, config files, Spring environments, stores, or any source that can return
036 * {@code missing()} without memoizing (for example during a static initializer).
037 * Do not wrap a {@code SensitivePropertySource} either: the wrapper hides the redaction marker and keeps secret values in
038 * memory for its lifetime.
039 *
040 * <h5 class='section'>Soft cap:</h5>
041 * <p>
042 * The cache holds at most {@link #MAX_ENTRIES} names. Once full, new names pass through to the delegate uncached; there
043 * is no eviction, so the first names seen win. The check is not locked: concurrent first lookups can overshoot the cap
044 * by at most the number of racing threads.
045 *
046 * <h5 class='section'>Null names:</h5>
047 * <p>
048 * A {@code null} name skips the cache (it cannot hold a {@code null} key) and goes to the delegate, so each delegate
049 * keeps its own {@code null} behavior.
050 *
051 * @since 10.0.0
052 */
053public final class CachingPropertySource implements PropertySource {
054
055   /**
056    * The maximum number of names cached by one instance.
057    */
058   public static final int MAX_ENTRIES = 4096;
059
060   private final PropertySource delegate;
061   private final ConcurrentHashMap<String,PropertyLookupResult> cache = new ConcurrentHashMap<>();
062
063   /**
064    * Wraps the specified source.
065    *
066    * @param delegate The source to wrap. Its answers must never change. Must not be <jk>null</jk>.
067    * @return A new caching source.
068    */
069   public static CachingPropertySource of(PropertySource delegate) {
070      return new CachingPropertySource(reqnn("delegate", delegate));
071   }
072
073   private CachingPropertySource(PropertySource delegate) {
074      this.delegate = delegate;
075   }
076
077   @Override /* PropertySource */
078   public PropertyLookupResult get(String name) {
079      if (name == null)
080         return delegate.get(null);
081      var cached = cache.get(name);
082      if (cached != null)
083         return cached;
084      var result = delegate.get(name);
085      if (cache.size() < MAX_ENTRIES) {
086         var previous = cache.putIfAbsent(name, result);
087         if (previous != null)
088            return previous;
089      }
090      return result;
091   }
092
093   /** Returns the wrapped source. For tests that check which sources are wrapped; not public API. */
094   PropertySource delegate() {
095      return delegate;
096   }
097
098   /** Returns the number of cached names. For tests; not public API. */
099   int size() {
100      return cache.size();
101   }
102}