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}