framework/base/src/org/ofbiz/base/util/cache/UtilCache.java
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*******************************************************************************/
package org.ofbiz.base.util.cache;
import java.io.NotSerializableException;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.ilscipio.scipio.ce.util.collections.ResourceBundleMap;
import org.ofbiz.base.concurrent.ExecutionPool;
import org.ofbiz.base.util.*;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import com.googlecode.concurrentlinkedhashmap.EvictionListener;
/**
* Generalized caching utility, heavily modified for Scipio.
* <p>
* FIXME: Although cache operations are thread-safe, UtilCache instances themselves are not automatically thread-safe
* and may need to be safe-published in static variables and thread-safe containers.
* </p>
* <ul>Caching features:
* <li>Limited or unlimited element capacity</li>
* <li>If limited, removes elements with the LRU (Least Recently Used) algorithm</li>
* <li>Keeps track of when each element was loaded into the cache</li>
* <li>Using the expireTime can report whether a given element has expired</li>
* <li>Counts misses and hits</li>
* </ul>
* <p>SCIPIO: 2.1.0: Now provides {@link Builder} through {@link #builder}.</p>
* <p>SCIPIO: 2.1.0: Synchronized blocks and methods for {@link #removeInternal} and bulk remove calls other than
* {@link #erase()} and {@link Index} operations have been completely removed to eliminate contention. They were largely
* unnecessary for {@link ConcurrentHashMap} in the absence of listeners and stats accuracy, and use of listeners is
* highly discouraged and considered deprecated.</p>
* <p>SCIPIO: 2.1.0: Execution pulses are no longer removed; because cache lines are removed by object identity rather
* than by key, it is possible to let cache lines expire as this lessens the removal processing cost at the expense
* of temporarily higher memory usage from the extra cache lines pending expiry. Problems can be prevented by using
* {@link #putIfAbsent(Object, Object)} rather than {@link #put(Object, Object)} in client code and in corresponding
* UtilCache calls, which avoids cross-thread publishing of temporary instances in general and so that fewer extra
* cache line instances and pulses are created. Please use {@link #putIfAbsent(Object, Object)} over
* {@link #put(Object, Object)} wherever possible.</p>
* <p>SCIPIO: 2.1.0: Added experimental index support using {@link Index}, which currently forces the use of
* non-concurrent synchronization and must be programmed via cache.properties index key definitions and is not yet
* recommended for use.</p>
*/
@SuppressWarnings("serial")
public class UtilCache<K, V> implements Serializable, EvictionListener<Object, CacheLine<V>> {
public static final String SEPARATOR = "::"; // cache key separator
public static final Pattern SEPARATOR_PAT = Pattern.compile(SEPARATOR); // SCIPIO: 2.1.0: Added
/**
* Default system cache configuration properties resource name, normally cache.properties with stripped extension.
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public static final String CFG_RES = "cache";
private static final Debug.OfbizLogger module = Debug.getOfbizLogger(java.lang.invoke.MethodHandles.lookup().lookupClass());
/** A static Map to keep track of all of the UtilCache instances. */
private static final ConcurrentHashMap<String, UtilCache<?, ?>> utilCacheTable = new ConcurrentHashMap<>();
/** An index number appended to utilCacheTable names when there are conflicts. */
private static final ConcurrentHashMap<String, AtomicInteger> defaultIndices = new ConcurrentHashMap<>();
/** The name of the UtilCache instance, is also the key for the instance in utilCacheTable. */
protected final String name;
protected final String baseName;
/**
* True if cache is enabled or false if disabled, in which case {@link #put} or {@link #putIfAbsent} have no effect
* and {@link #get(Object)} operations always return null.
* <p>Can be set false using explicit "enabled=false" cache property or {@link #setEnabled(boolean)}, and supports
* real-time toggling only insofar as this flag is not <code>volatile</code>, meaning the jvm publishes the toggling
* lazily to other threads with no time guarantee.</p>
* <p>SCIPIO: 2.0.0: Added.</p>
*/
protected boolean enabled;
/**
* The maximum number of elements in the cache.
* <p>If set to 0, there will be no limit on the number of elements in the cache.</p>
*/
protected int sizeLimit;
protected int maxInMemory;
/**
* Specifies the amount of time since initial loading before an element will be reported as expired.
* <p>If set to 0, elements will never expire.</p>
*/
protected long expireDelayNanos;
/** Specifies whether or not to use soft references for this cache, defaults to false */
protected boolean useSoftReference;
/**
* Main key format for keys in the {@link #memoryTable}, in the form: <code>delegator::productId::...</code>.
* <p>SCIPIO: 2.1.0: Added for {@link UtilCache.Index} support: added keyFormat definition for use in defining index keys from main cache keys.</p>
*/
protected String keyFormat;
/**
* Static index definitions, from property or builder.
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support.</p>
*/
protected Map<String, Object> indexDefs;
/**
* The set of listeners to receive notifications when items are modified (either deliberately or because they were expired).
* <p>SCIPIO: NOTE: Should now have volatile, but never used in serious code and typically can be safe-published
* by holding class (static variable), and often publishing time will be arbitrary to begin with.</p>
* <p>SCIPIO: 2.1.0: Optimized with empty.</p>
*/
protected Collection<CacheListener<K, V>> listeners = Collections.emptySet();
protected ConcurrentMap<Object, CacheLine<V>> memoryTable;
/**
* Indexes by name whose index keys map to main keys in the {@link #memoryTable}.
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support.</p>
*/
protected final Map<String, Index> indexNameMap;
/**
* Indexes whose index keys map to main keys in the {@link #memoryTable}.
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support.</p>
*/
protected final List<Index> indexList;
/**
* Used to exclude {@link #erase()} and {@link #clear()} operations for performance reasons (not essential).
*/
protected final Object eraseLockObj = new Serializable(){};
/** A count of the number of cache hits */
protected final AtomicLong hitCount = new AtomicLong(0);
/** A count of the number of cache misses because it is not found in the cache */
protected final AtomicLong missCountNotFound = new AtomicLong(0);
/** A count of the number of cache misses because it expired */
protected final AtomicLong missCountExpired = new AtomicLong(0);
/** A count of the number of cache misses because it was cleared from the Soft Reference (ie garbage collection, etc) */
protected final AtomicLong missCountSoftRef = new AtomicLong(0);
/** A count of the number of cache hits on removes */
protected final AtomicLong removeHitCount = new AtomicLong(0);
/** A count of the number of cache misses on removes */
protected final AtomicLong removeMissCount = new AtomicLong(0);
/**
* Main builder constructor.
* <p>SCIPIO: 2.1.0: Created Builder-based constructor for {@link Index} support: added keyFormat definition for use
* in defining index keys from main cache keys.</p>
*/
protected UtilCache(Builder<K, V> builder) {
this.baseName = builder.baseNameOrDefault();
this.name = builder.nameOrDefault();
this.enabled = (builder.enabled() != null) ? builder.enabled() : true;
this.sizeLimit = (builder.sizeLimit() != null) ? builder.sizeLimit() : 0;
this.maxInMemory = (builder.maxInMemory() != null) ? builder.maxInMemory() : 0;
this.expireDelayNanos = (builder.expireDelay() != null) ? TimeUnit.NANOSECONDS.convert(builder.expireDelay(), TimeUnit.MILLISECONDS) : 0L;
this.useSoftReference = (builder.useSoftReference() != null) ? builder.useSoftReference() : false;
this.keyFormat = UtilValidate.nullIfEmpty(builder.keyFormat());
this.indexDefs = UtilValidate.isNotEmpty(builder.indexDefs()) ? builder.indexDefs() : Collections.emptyMap();
int maxMemSize = this.maxInMemory;
if (maxMemSize == 0) {
maxMemSize = sizeLimit;
}
if (maxMemSize == 0) {
this.memoryTable = new ConcurrentHashMap<>();
} else {
this.memoryTable = new ConcurrentLinkedHashMap.Builder<Object, CacheLine<V>>()
.maximumWeightedCapacity(maxMemSize)
.listener(this)
.build();
}
Map<String, Index> indexNameMap = Collections.emptyMap();
List<Index> indexList = Collections.emptyList();
if (UtilValidate.isNotEmpty(this.keyFormat) && UtilValidate.isNotEmpty(this.indexDefs)) {
indexNameMap = makeIndexes(this.indexDefs);
if (!indexNameMap.isEmpty()) {
indexList = new ArrayList<>(indexNameMap.values());
} else {
indexNameMap = Collections.emptyMap();
}
}
this.indexNameMap = indexNameMap;
this.indexList = indexList;
}
/**
* If the cache already exists by name, return it; otherwise create using supplier and store; if supplier is
* {@link Builder} then {@link Builder#copy()}, {@link Builder#baseNameIfSet(String)} and {@link Builder#build()} are called.
* <p>NOTE: Builder instances are copied and left untouched so they can be safely published and reused in static variables.</p>
* <p>NOTE: For this method, the global cache table key is the simple cache name, without sequence index number appended.
* The cache supplier is only consulted if the cache's simple name is not already in the global cache table.</p>
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support with enhanced Builder which can be returned by supplier.</p>
* @param baseName The logical cache name
* @param supplier A UtilCache or {@link Builder} instance that will have {@link Builder#baseNameIfSet(String)} and {@link Builder#build()} called
*/
public static <K, V> UtilCache<K, V> getOrCreate(String baseName, Supplier<?> supplier) {
return getOrCreateAuto(baseName, supplier);
}
/**
* If the cache already exists by name, return it; otherwise creates with builder on which {@link Builder#copy()},
* {@link Builder#baseNameIfSet(String)} and {@link Builder#build()} are called.
* <p>NOTE: Builder instances are copied and left untouched so they can be safely published and reused in static variables.</p>
* <p>NOTE: For this method, the global cache table key is the simple cache name, without sequence index number appended.
* The cache supplier is only consulted if the cache's simple name is not already in the global cache table.</p>
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support with enhanced Builder which can be returned by supplier.</p>
* @param baseName The logical cache name
* @param builder A {@link Builder} instance that will have {@link Builder#baseName(String)} and {@link Builder#build()} called
*/
public static <K, V> UtilCache<K, V> getOrCreate(String baseName, Builder<K, V> builder) {
return getOrCreateAuto(baseName, builder);
}
protected static <K, V> UtilCache<K, V> getOrCreateAuto(String baseName, Object cacheBuilderSupplier) {
UtilCache<K, V> cache = UtilGenerics.cast(utilCacheTable.get(baseName));
if (cache != null) {
return cache;
}
cache = build(baseName, cacheBuilderSupplier);
UtilCache<K, V> prevCache = UtilGenerics.cast(utilCacheTable.putIfAbsent(baseName, cache));
return (prevCache != null) ? prevCache : cache;
}
/**
* Creates a UtilCache using given supplier and stores; if supplier returns a {@link Builder} then {@link Builder#baseNameIfSet(String)}
* and {@link Builder#build()} are called.
* <p>NOTE: Builder instances are copied and left untouched so they can be safely published and reused in static variables.</p>
* <p>NOTE: For this method, the global cache table key is the full cache name, with sequence index number appended.
* The cache supplier is always called and the cache is always added to the global table.</p>
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support with enhanced Builder which can be returned by supplier.</p>
* @param baseName The logical cache name
* @param supplier A supplier of a {@link Builder} or UtilCache
*/
public static <K, V> UtilCache<K, V> create(String baseName, Supplier<?> supplier) {
return createAuto(baseName, supplier);
}
/**
* Creates a UtilCache using given supplier and stores; {@link Builder#baseNameIfSet(String)} and {@link Builder#build()} are called.
* <p>NOTE: Builder instances are copied and left untouched so they can be safely published and reused in static variables.</p>
* <p>NOTE: For this method, the global cache table key is the full cache name, with sequence index number appended.
* The cache supplier is always called and the cache is always added to the global table.</p>
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support with enhanced Builder which can be returned by supplier.</p>
* @param baseName The logical cache name
* @param builder A {@link Builder} instance
*/
public static <K, V> UtilCache<K, V> create(String baseName, Builder<K, V> builder) {
return createAuto(baseName, builder);
}
protected static <K, V> UtilCache<K, V> createAuto(String name, Object cacheBuilderSupplier) {
UtilCache<K, V> cache = build(name, cacheBuilderSupplier);
utilCacheTable.put(cache.getName(), cache); // NOTE: Here uses full cache name (not simple name)
return cache;
}
/**
* Builds a UtilCache using given supplier without storing; if supplier provides a {@link Builder} then
* {@link Builder#baseNameIfSet(String)} and {@link Builder#build()} are called.
* <p>NOTE: Builder instances are copied and left untouched so they can be safely published and reused in static variables.</p>
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support with enhanced Builder which can be returned by supplier.</p>
* @param baseName The cache's simple name
* @param cacheBuilderSupplier A {@link Builder} or {@link UtilCache} instance, or a {@link Supplier} for one of these
*/
protected static <K, V> UtilCache<K, V> build(String baseName, Object cacheBuilderSupplier) {
if (cacheBuilderSupplier instanceof Supplier) {
cacheBuilderSupplier = ((Supplier<?>) cacheBuilderSupplier).get();
}
if (cacheBuilderSupplier instanceof Builder) {
// NOTE: This makes a copy of the builder to leave the original intact, which is in most cases
// great because it allows the caller to reuse Builder definitions without the overhead at every getOrCreate() call
// and rather only at creation time. Even though it slows down the call, it allows safe scoping and reuse
// and UtilCache instances are rarely created in practice.
return UtilGenerics.<Builder<K, V>>cast(cacheBuilderSupplier).copy().baseNameIfSet(baseName).build();
} else if (cacheBuilderSupplier instanceof UtilCache) {
return UtilGenerics.cast(cacheBuilderSupplier);
} else {
throw new IllegalArgumentException("Invalid cache, builder or supplier; must be one of: " + Builder.class.getSimpleName() +
", " + Supplier.class.getSimpleName() + ", " + UtilCache.class.getSimpleName());
}
}
/**
* Returns a cache builder that automatically creates and registers a new cache in the system when the
* {@link Builder#build()} method is called to create the cache.
* <p>NOTE: It is recommended to pass this Builder as result of the supplier in a call to
* {@link #getOrCreate(String, Supplier)} or {@link #create(String, Supplier)} as they automatically set the name
* and avoid creating a builder or cache unless necessary using common patterns; note that
* {@link #getOrCreate(String, Supplier)} is recommended over {@link #create(String, Supplier)} because
* <code>putIfAbsent</code> is better than <code>put</code> to avoid unnecessary lingering references (since any new
* instances temporarily created are not published to other threads at all, instead of being published temporarily
* into the map and replaced).</p>
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public static <K, V> Builder<K, V> builder() {
return new Builder<>();
}
/**
* Returns a cache builder that automatically creates and registers a new cache in the system when the
* {@link Builder#build()} method is called to create the cache.
* <p>NOTE: It is recommended to pass this Builder as result of the supplier in a call to
* {@link #getOrCreate(String, Supplier)} or {@link #create(String, Supplier)} as they automatically set the name
* and avoid creating a builder or cache unless necessary using common patterns; note that
* {@link #getOrCreate(String, Supplier)} is recommended over {@link #create(String, Supplier)} because
* <code>putIfAbsent</code> is better than <code>put</code> to avoid unnecessary lingering references (since any new
* instances temporarily created are not published to other threads at all, instead of being published temporarily
* into the map and replaced).</p>
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public static <K, V> Builder<K, V> builder(String name) {
return new Builder<>(name);
}
/**
* Builder for UtilCache with easy defaults.
* <p>NOTE: Instances are thread-safe and reusable, but only if safely published in static variable and thread-safe
* collections</p>
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public static class Builder<K, V> {
String baseName;
String resource;
Boolean autoScope;
List<String> propNames;
Boolean autoProps;
Boolean enabled;
Integer sizeLimit;
Integer maxInMemory;
Long expireDelay;
Boolean useSoftReference;
String keyFormat;
Map<String, Object> indexDefs;
// mutable/transient
String name;
Integer seqNum;
ResourceBundle resourceBundle;
public Builder() {}
public Builder(String baseName) {
baseName(baseName);
}
public Builder(Builder<K, V> other) {
this.baseName = other.baseName;
this.resource = other.resource;
this.autoScope = other.autoScope;
this.propNames = other.propNames;
this.autoProps = other.autoProps;
this.enabled = other.enabled;
this.sizeLimit = other.sizeLimit;
this.maxInMemory = other.maxInMemory;
this.expireDelay = other.expireDelay;
this.useSoftReference = other.useSoftReference;
this.keyFormat = other.keyFormat;
this.indexDefs = other.indexDefs;
// mutable/transient
this.name = other.name; // may also be manually set so must be preserved
this.seqNum = null;
this.resourceBundle = null;
}
/**
* Creates a copy of this Builder, with mutable/transient fields not or only partially preserved as appropriate.
* <p>NOTE: Despite the extra copies, it is recommended to use this method as a scoping mechanism, as this
* easily to easily reuse static Builder definitions without modifying them, especially before calling any of
* the methods that modify any of the mutable/transient fields. This can normally be done before any
* name-resolution ({@link #nameOrDefault()}), build ({@link #build()}) or constructor (UtilCache(Builder)) calls.</p>
*/
public Builder<K, V> copy() {
return new Builder<>(this);
}
/**
* Creates a cache instance without registering in the system.
* <p>Does not store the cache; intended for use with {@link #getOrCreate(String, Supplier)}.</p>
*/
public UtilCache<K, V> build() {
if (!Boolean.FALSE.equals(autoProps())) {
fromProperties();
}
return new UtilCache<>(this);
}
public String baseName() {
return baseName;
}
public Builder<K, V> baseName(String baseName) {
this.baseName = baseName;
return this;
}
public Builder<K, V> baseNameIfSet(String baseName) {
return UtilValidate.isNotEmpty(baseName) ? baseName(baseName) : this;
}
public String baseNameOrDefault() {
String name = this.baseName();
return (name != null) ? name : "default";
}
public String name() {
return name;
}
/**
* Overrides the full name, which is normally the name concatenated with a generated number.
*/
public Builder<K, V> name(String name) {
this.name = name;
return this;
}
public String nameOrDefault() {
String fullName = this.name();
if (fullName == null) {
fullName = baseNameOrDefault();
int seqNum = nextSeqNum();
if (seqNum != 0) {
fullName += seqNum + "";
}
this.name = fullName;
}
return fullName;
}
public String resource() {
return resource;
}
public Builder<K, V> resource(String resource) {
this.resource = resource;
return this;
}
public Boolean autoScope() {
return autoScope;
}
/**
* If true (default), factory methods like {@link #getOrCreate}, {@link #create), {@link #build} automatically
* scope their changes using {@link #copy()} calls to prevent modifying the builders they receive as parameters;
* if false, no scoping is automatically applied.
* <p>NOTE: Usually this is a small performance concern so it's better to have on.</p>
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public Builder<K, V> autoScope(Boolean autoScope) {
this.autoScope = autoScope;
return this;
}
public String resourceOrDefault() {
return (this.resource() != null) ? this.resource() : CFG_RES;
}
public ResourceBundle resourceBundle() {
ResourceBundle resourceBundle = this.resourceBundle;
if (resourceBundle == null) {
resourceBundle = ResourceBundle.getBundle(resourceOrDefault());
this.resourceBundle = resourceBundle;
}
return resourceBundle;
}
/**
* The cache.properties or specified {@link #resource()} property names to consult for values, with priority
* lookup to first in list.
*/
public List<String> propNames() {
return propNames;
}
public Builder<K, V> propNames(Collection<String> propNames) {
this.propNames = (propNames != null) ? (!propNames.isEmpty() ? new ArrayList<>(propNames) : Collections.emptyList()) : null;
return this;
}
public Builder<K, V> propNames(String... propNames) {
this.propNames = (propNames != null && propNames.length > 0) ? new ArrayList<>(Arrays.asList(propNames)) : null;
return this;
}
public Boolean autoProps() {
return autoProps;
}
public Builder<K, V> autoProps(Boolean autoProps) {
this.autoProps = autoProps;
return this;
}
public Boolean enabled() {
return enabled;
}
public Builder<K, V> enabled(Boolean enabled) {
this.enabled = enabled;
return this;
}
public Integer sizeLimit() {
return sizeLimit;
}
public Builder<K, V> sizeLimit(Integer sizeLimit) {
this.sizeLimit = sizeLimit;
return this;
}
public Integer maxInMemory() {
return maxInMemory;
}
public Builder<K, V> maxInMemory(Integer maxInMemory) {
this.maxInMemory = maxInMemory;
return this;
}
public Long expireDelay() {
return expireDelay;
}
public Builder<K, V> expireDelay(Long expireDelay) {
this.expireDelay = expireDelay;
return this;
}
public Boolean useSoftReference() {
return useSoftReference;
}
public Builder<K, V> useSoftReference(Boolean useSoftReference) {
this.useSoftReference = useSoftReference;
return this;
}
public String keyFormat() {
return keyFormat;
}
public Builder<K, V> keyFormat(String keyFormat) {
this.keyFormat = keyFormat;
return this;
}
public Map<String, Object> indexDefs() {
return indexDefs;
}
public Builder<K, V> indexDefs(Map<String, ?> indexDefs) {
this.indexDefs = UtilGenerics.cast(indexDefs);
return this;
}
public Builder<K, V> indexDefs(Object... indexDefs) {
return indexDefs(UtilMisc.orderedMap(indexDefs));
}
public Integer seqNum() {
return this.seqNum;
}
public int nextSeqNum() {
Integer seqNum = this.seqNum();
if (seqNum == null) {
seqNum = getNextDefaultIndexNumber(baseNameOrDefault());
this.seqNum = seqNum;
}
return seqNum;
}
/**
* Reads extra cache properties from cache.properties or other resource and replaces any explicitly specied
* or empty values as appropriate.
* <p>SCIPIO: 2.1.0: Replaces setPropertiesParams; automatically called by {@link #build()}.</p>
*/
public Builder<K, V> fromProperties() {
Collection<String> propNames = this.propNames();
return fromProperties((propNames != null) ? propNames : UtilMisc.toList(baseNameOrDefault()));
}
/**
* Reads extra cache properties from cache.properties or other resource and replaces any explicitly specied
* or empty values as appropriate.
* <p>SCIPIO: 2.1.0: Replaces setPropertiesParams; automatically called by {@link #build()}.</p>
*/
public Builder<K, V> fromProperties(String... propNames) {
return fromProperties(resourceBundle(), propNames);
}
/**
* Reads extra cache properties from cache.properties or other resource and replaces any explicitly specied
* or empty values as appropriate.
* <p>SCIPIO: 2.1.0: Replaces setPropertiesParams; automatically called by {@link #build()}.</p>
*/
public Builder<K, V> fromProperties(Collection<String> propNames) {
return fromProperties(resourceBundle(), propNames);
}
/**
* Reads extra cache properties from cache.properties or other resource and replaces any explicitly specied
* or empty values as appropriate.
* <p>SCIPIO: 2.1.0: Replaces setPropertiesParams; automatically called by {@link #build()}.</p>
*/
public Builder<K, V> fromProperties(ResourceBundle res, String... propNames) {
return fromProperties(res, Arrays.asList(propNames));
}
/**
* Reads extra cache properties from cache.properties or other resource and replaces any explicitly specied
* or empty values as appropriate.
* <p>SCIPIO: 2.1.0: Replaces setPropertiesParams; automatically called by {@link #build()}.</p>
*/
public Builder<K, V> fromProperties(ResourceBundle res, Collection<String> propNames) {
if (UtilValidate.isEmpty(propNames)) {
return this;
}
String value;
value = getPropertyParam(res, propNames, "enabled");
if (value != null) {
this.enabled(UtilMisc.booleanValue(value));
}
// SCIPIO: 2.1.0: Now supports "sizeLimit", but "maxSize" remains supported
value = getPropertyParam(res, propNames, "sizeLimit");
if (UtilValidate.isEmpty(value)) {
value = getPropertyParam(res, propNames, "maxSize");
}
if (UtilValidate.isNotEmpty(value)) {
this.sizeLimit(Integer.parseInt(value));
}
value = getPropertyParam(res, propNames, "maxInMemory");
if (UtilValidate.isNotEmpty(value)) {
this.maxInMemory(Integer.parseInt(value));
}
// SCIPIO: 2.1.0: Now prefers "expireDelay", but "expireTime" remains supported
value = getPropertyParam(res, propNames, "expireDelay");
if (UtilValidate.isEmpty(value)) {
value = getPropertyParam(res, propNames, "expireTime");
}
if (UtilValidate.isNotEmpty(value)) {
this.expireDelay(Long.parseLong(value));
}
value = getPropertyParam(res, propNames, "useSoftReference");
if (value != null) {
this.useSoftReference(UtilMisc.booleanValue(value));
}
// SCIPIO: 2.1.0: keyFormat and indexDefs
value = getPropertyParam(res, propNames, "keyFormat");
if (UtilValidate.isNotEmpty(value)) {
this.keyFormat(value);
Map<String, Object> ikf = makeSubPropertyParamsWithId(res, propNames, "index");
if (UtilValidate.isNotEmpty(ikf)) {
Map<String, Object> indexDefs = UtilGenerics.cast(this.indexDefs());
if (indexDefs != null) {
Map<String, Object> ikfNew = new LinkedHashMap<>(indexDefs);
for (Map.Entry<String, Object> entry : ikf.entrySet()) {
Object ikfValueOld = ikfNew.get(entry.getKey());
if (ikfValueOld == null) {
ikfNew.put(entry.getKey(), entry.getValue());
} else if (ikfValueOld instanceof Map) {
Map<String, Object> newMap = new LinkedHashMap<>(UtilGenerics.cast(ikfValueOld));
newMap.putAll(UtilGenerics.cast(entry.getValue()));
ikfNew.put(entry.getKey(), newMap);
} else if (ikfValueOld instanceof String) {
Map<String, Object> newMap = UtilMisc.toMap("keyFormat", ikfValueOld);
newMap.putAll(UtilGenerics.cast(entry.getValue()));
ikfNew.put(entry.getKey(), newMap);
} else {
Debug.logError("Invalid index defs for key [" + entry.getKey() + "]: " + indexDefs, module);
}
}
this.indexDefs(ikfNew);
} else {
this.indexDefs(ikf);
}
}
}
return this;
}
}
private static String getNextDefaultIndex(String cacheName) {
int i = getNextDefaultIndexNumber(cacheName);
return i == 0 ? "" : Integer.toString(i);
}
private static int getNextDefaultIndexNumber(String cacheName) { // SCIPIO: 2.1.0: Refactored
AtomicInteger curInd = defaultIndices.get(cacheName);
if (curInd == null) {
defaultIndices.putIfAbsent(cacheName, new AtomicInteger(0));
curInd = defaultIndices.get(cacheName);
}
return curInd.getAndIncrement();
}
/**
* Gets the named property from the resource bundle using the given propNames as prefix, where the last entry
* has priority over the first.
* <p>SCIPIO: 2.1.0: Refactored and inverted main loop: in most cases only up to 2 propNames total were normally
* specified (["default", baseName]) and because the first propName was separate, these produced 2 calls to
* getPropertyParam so effectively the second overrode the first correctly; however this method itself was written
* backward such that any extra propNames after the first 2 (including the original dedicated propName method
* parameter) do not get priority, which was highly confusing from the point of view of client code. We have fixed
* this situation by having only one propNames list (no separate propName) that preserves backward-compatibility for
* the most cases (total propNames length 1 or 2).</p>
*/
protected static String getPropertyParam(ResourceBundle res, Collection<String> propNames, String parameter) {
try {
// SCIPIO: 2.1.0: Reverted loop
List<String> propNamesList = UtilMisc.toList(propNames);
ListIterator<String> it = propNamesList.listIterator(propNames.size());
while(it.hasPrevious()) {
String propName = it.previous();
if (UtilValidate.isEmpty(propName)) {
continue; // SCIPIO: 2.1.0: More friendly
}
String key = propName.concat(".").concat(parameter);
if (res.containsKey(key)) {
try {
return res.getString(key);
} catch (MissingResourceException e) {
}
}
}
} catch (Exception e) {
Debug.logWarning(e, "Error getting " + parameter + " value from ResourceBundle for propNames: " + propNames, module);
}
return null;
}
protected static Map<String, Object> makeSubPropertyParamsWithId(ResourceBundle res, Collection<String> propNames, String parameter) { // SCIPIO: 2.1.0: Added
try {
Map<String, Map<String, Object>> paramMap = new LinkedHashMap<>();
Map<String, Object> resMap = new ResourceBundleMap(res);
// SCIPIO: 2.1.0: NOTE: This is the opposite order of getPropertyParam() because it always replaces entries
// instead of finding the first non-null.
for (String propName : propNames) {
if (UtilValidate.isEmpty(propName)) {
continue;
}
try {
UtilProperties.extractPropertiesWithPrefixAndId(paramMap, resMap, propName + "." + parameter + ".");
} catch (MissingResourceException e) {
}
}
return UtilGenerics.cast(paramMap);
} catch (Exception e) {
Debug.logWarning(e, "Error getting " + parameter + " value from ResourceBundle for propNames: " + propNames, module);
}
return null;
}
private Object fromKey(Object key) {
return key == null ? ObjectType.NULL : key;
}
private K toKey(Object key) {
return key == ObjectType.NULL ? null : UtilGenerics.cast(key);
}
public Object getCacheLineTable() {
throw new UnsupportedOperationException();
}
public boolean isEmpty() {
return memoryTable.isEmpty();
}
/**
* Puts or loads the passed element into the cache.
* @param key The key for the element, used to reference it in the hashtables and LRU linked list
* @param value The value of the element
*/
public V put(K key, V value) {
return putInternal(key, value, expireDelayNanos);
}
public V putIfAbsent(K key, V value) {
return putIfAbsentInternal(key, value, expireDelayNanos);
}
public V putIfAbsentAndGet(K key, V value) {
V cachedValue = putIfAbsent(key, value);
return (cachedValue != null) ? cachedValue : value;
}
/**
* Creates cache line.
* <p>SCIPIO: 2.1.0: To avoid pulses expiring before the cache line is added to memoryTable, this has been split
* so that the initial cache line creation defers the {@link #registerExpirePulse(CacheLine)} call to after the memoryTable
* put calls; this also avoids putIfAbsent unnecessarily adding execution pulses that are never actually added to
* memoryTable.</p>
*/
protected CacheLine<V> createCacheLine(K key, V value, long expireDelayNanos) {
long loadTimeNanos = expireDelayNanos > 0 ? System.nanoTime() : 0;
if (useSoftReference) {
return createSoftRefCacheLine(key, value, loadTimeNanos, expireDelayNanos);
}
return createHardRefCacheLine(key, value, loadTimeNanos, expireDelayNanos);
}
/**
* Common UtilCache cache line with <code>UtilCache.this</code> reference.
* <p>SCIPIO: 2.1.0: Refactored and added Serializable.</p>
*/
protected abstract class Line extends CacheLine<V> implements Serializable {
protected final Object key;
public Line(Object key, V value, long loadTimeNanos, long expireDelayNanos) {
super(loadTimeNanos, expireDelayNanos);
this.key = key;
}
@Override
public <R> R getKey() { // SCIPIO: 2.1.0: Added for UtilCache.Index support
return UtilGenerics.cast(key);
}
@Override
protected void remove() {
removeInternal(key, this, false);
}
}
/**
* Creates soft-ref cache line.
* <p>SCIPIO: 2.1.0: Now omits registering pulses, which is left to callers to do after adding to memoryTable
* via {@link #registerExpirePulse(CacheLine)}.</p>
*/
protected CacheLine<V> createSoftRefCacheLine(Object key, V value, long loadTimeNanos, long expireDelayNanos) {
return new SoftLine(key, value, loadTimeNanos, expireDelayNanos);
}
/**
* Soft reference cache line.
* <p>SCIPIO: 2.1.0: Moved from SoftRefCacheLine abstract class plus parts extracted from former createSoftRefCacheLine method.</p>
*/
protected class SoftLine extends Line {
protected final CacheSoftReference<V> ref;
public SoftLine(Object key, V value, long loadTimeNanos, long delayNanos) {
super(key, value, loadTimeNanos, delayNanos);
this.ref = new CacheSoftReference<V>(value) {
@Override
public void remove() {
//Debug.logInfo("Cache [" + UtilCache.this.name + "]: Removing soft ref [" + key + "]", module);
SoftLine.this.remove();
}
};
}
@Override
public V getValue() {
return ref.get();
}
@Override
protected void cancel() {
ref.clear();
}
@Override
protected CacheLine<V> changeLine(boolean useSoftReference, long expireDelayNanos) {
if (useSoftReference) {
if (!differentExpireTime(expireDelayNanos)) { // SCIPIO: 2.1.0: Inverted this logic: should return same if same expire time
return this;
}
return createSoftRefCacheLine(getKey(), getValue(), getLoadTimeNanos(), expireDelayNanos);
}
return createHardRefCacheLine(getKey(), getValue(), getLoadTimeNanos(), expireDelayNanos);
}
}
/**
* Creates hard-ref cache line.
* <p>SCIPIO: 2.1.0: Now omits registering pulses, which is left to callers to do after adding to memoryTable
* via {@link #registerExpirePulse(CacheLine)}.</p>
*/
protected CacheLine<V> createHardRefCacheLine(Object key, V value, long loadTimeNanos, long expireDelayNanos) {
return new HardLine(key, value, loadTimeNanos, expireDelayNanos);
}
/**
* Hard reference cache line.
* <p>SCIPIO: 2.1.0: Moved from HardRefCacheLine abstract class plus parts extracted from former createHardRefCacheLine method.</p>
*/
protected class HardLine extends Line {
protected final V value;
public HardLine(Object key, V value, long loadTimeNanos, long expireDelayNanos) {
super(key, value, loadTimeNanos, expireDelayNanos);
this.value = value;
}
@Override
public V getValue() {
return value;
}
@Override
protected CacheLine<V> changeLine(boolean useSoftReference, long expireDelayNanos) {
if (useSoftReference) {
return createSoftRefCacheLine(getKey(), getValue(), getLoadTimeNanos(), expireDelayNanos);
}
if (!differentExpireTime(expireDelayNanos)) { // SCIPIO: 2.1.0: Inverted this logic: should return same if same expire time
return this;
}
return createHardRefCacheLine(getKey(), getValue(), getLoadTimeNanos(), expireDelayNanos);
}
}
/**
* Registers the cache line as execution pulse for delayed removal if enabled.
* <p>SCIPIO: 2.1.0: Renamed from <code>tryRegister</code> and removed loadTimeNanos parameter since already recorded in cache line.</p>
*/
private CacheLine<V> registerExpirePulse(CacheLine<V> line) {
if (line.getLoadTimeNanos() > 0) {
ExecutionPool.addPulse(line);
}
return line;
}
private V cancel(CacheLine<V> line) {
// FIXME: this is a race condition, the item could expire
// between the time it is replaced, and it is cancelled
V oldValue = line.getValue();
// SCIPIO: 2.1.0: Becomes expensive and is not strictly needed because deletions are by CacheLine not by key and putIfAbsent() helps
//ExecutionPool.removePulse(line);
line.cancel();
return oldValue;
}
/**
* Puts or loads the passed element into the cache.
* @param key The key for the element, used to reference it in the hashtables and LRU linked list
* @param value The value of the element
* @param expireDelayMillis how long to keep this key in the cache
*/
public V put(K key, V value, long expireDelayMillis) {
return putInternal(key, value, TimeUnit.NANOSECONDS.convert(expireDelayMillis, TimeUnit.MILLISECONDS));
}
public V putIfAbsent(K key, V value, long expireDelayMillis) {
return putIfAbsentInternal(key, value, TimeUnit.NANOSECONDS.convert(expireDelayMillis, TimeUnit.MILLISECONDS));
}
V putInternal(K key, V value, long expireDelayNanos) {
if (!enabled) {
return null; // SCIPIO: 2018-03: no-op
}
// SCIPIO: 2.1.0: Now defer the initial pulse register to after memoryTable.put (see createCacheLine)
CacheLine<V> newCacheLine = createCacheLine(key, value, expireDelayNanos);
if (!indexList.isEmpty()) { // SCIPIO: 2.1.0: Added for UtilCache.Index support
// NOTE: Cache lines must be added to index before adding to memoryTable to guarantee lifetime visibility
// due to lack of synchronization
addCacheLineToIndexes(newCacheLine);
}
CacheLine<V> oldCacheLine = memoryTable.put(fromKey(key), newCacheLine);
registerExpirePulse(newCacheLine);
V oldValue = oldCacheLine == null ? null : cancel(oldCacheLine);
if (oldValue == null) {
noteAddition(key, value);
return null;
}
noteUpdate(key, value, oldValue);
return oldValue;
}
V putIfAbsentInternal(K key, V value, long expireDelayNanos) {
if (!enabled) {
return null; // SCIPIO: 2018-03: no-op
}
V oldValue;
// SCIPIO: 2.1.0: Now defer the initial pulse register to after memoryTable.put (see createCacheLine)
CacheLine<V> newCacheLine = createCacheLine(key, value, expireDelayNanos);
if (!indexList.isEmpty()) { // SCIPIO: 2.1.0: Added for UtilCache.Index support
// NOTE: Cache lines must be added to index before adding to memoryTable to guarantee lifetime visibility
// due to lack of synchronization, and for putIfAbsent must be done even if the cache line is not
// actually added to memoryTable otherwise lifetime is not fully covered.
addCacheLineToIndexes(newCacheLine);
}
CacheLine<V> oldCacheLine = memoryTable.putIfAbsent(fromKey(key), newCacheLine);
if (oldCacheLine == null) {
oldValue = null;
// SCIPIO: 2.1.0: As above, now simply run tryRegister() here after the line is actually added
registerExpirePulse(newCacheLine);
} else {
if (!indexList.isEmpty()) { // SCIPIO: 2.1.0: Added for UtilCache.Index support
// NOTE: In this case the cache line was not added, so remove it immediately
removeCacheLineFromIndexes(newCacheLine);
}
oldValue = oldCacheLine.getValue();
// SCIPIO: 2.1.0: As above, there is no longer need to cancel the cache line since it was never registered.
//cancel(newCacheLine);
}
if (oldValue == null) {
noteAddition(key, value);
return null;
}
return oldValue;
}
/**
* Gets an element from the cache according to the specified key.
* @param key The key for the element, used to reference it in the hashtables and LRU linked list
* @return The value of the element specified by the key
*/
public V get(Object key) {
if (!enabled) {
return null; // SCIPIO: 2018-03: no-op
}
boolean countGet = true;
CacheLine<V> line = memoryTable.get(fromKey(key));
if (line == null) {
missCountNotFound.incrementAndGet();
} else {
if (countGet) {
hitCount.incrementAndGet();
}
}
return line != null ? line.getValue() : null;
}
public Collection<V> values() {
List<V> valuesList = new LinkedList<>();
for (CacheLine<V> line: memoryTable.values()) {
valuesList.add(line.getValue());
}
return valuesList;
}
private long findSizeInBytes(Object o, Object key) { // SCIPIO: Added key and improved logging
if (o == null) {
if (Debug.verboseOn()) {
Debug.logVerbose("Found null object in cache: " + getName(), module);
}
return 0;
}
try {
if (o instanceof Serializable) {
return UtilObject.getByteCount(o);
}
if (Debug.verboseOn()) {
Debug.logVerbose("Unable to compute memory size for non serializable object; returning 0 byte size for object of " + o.getClass()
+ " for key '" + key + "' in cache: " + getName(), module);
}
return 0;
} catch (NotSerializableException e) {
// this happens when we try to get the byte count for an object which itself is
// serializable, but fails to be serialized, such as a map holding unserializable objects
if (Debug.warningOn()) {
Debug.logWarning("NotSerializableException while computing memory size; returning 0 byte size for object of " + e.getMessage()
+ " for key '" + key + "' in cache: " + getName(), module);
}
return 0;
} catch (Exception e) {
Debug.logWarning(e, "Unable to compute memory size for object of " + o.getClass()
+ " for key '" + key + "' in cache: " + getName(), module);
return 0;
}
}
public long getSizeInBytes() {
long totalSize = 0;
// SCIPIO: Include key for debugging
//for (CacheLine<V> line: memoryTable.values()) {
// totalSize += findSizeInBytes(line.getValue());
//}
for (Map.Entry<Object, CacheLine<V>> lineEntry : memoryTable.entrySet()) {
totalSize += findSizeInBytes(lineEntry.getValue().getValue(), lineEntry.getKey());
}
return totalSize;
}
/**
* Counts all element from the cache matching the given filter.
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.0.0: Added.</p>
* <p>SCIPIO: 2.1.0: Removed synchronized because only used on remove calls and ConcurrentHashMap can usually
* handle bulk removal iterations without it.</p>
* @param entryFilter The entry filter - return true to remove key
*/
public int countByFilter(CacheEntryFilter<K, V> entryFilter) { // SCIPIO: 2.1.0: Removed synchronized
int count = 0;
for (Map.Entry<Object, CacheLine<V>> entry : memoryTable.entrySet()) {
if (entryFilter.filter(UtilGenerics.cast(toKey(entry.getKey())), UtilGenerics.cast(entry.getValue().getValue()))) {
count++;
}
}
return count;
}
/**
* Counts all element from the cache matching the given key regex pattern (uses {@link java.util.regex.Matcher#matches()} on each key).
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.1.0: Added.
*/
public int countByKeyPat(Pattern keyPat) {
return countByFilter(new KeyPatternCacheEntryFilter<>(keyPat));
}
/**
* Counts all element from the cache matching the given key regex pattern (uses {@link java.util.regex.Matcher#matches()} on each key).
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.1.0: Added.
*/
public int countByKeyPat(String keyPat) {
return countByFilter(new KeyPatternCacheEntryFilter<>(keyPat));
}
/**
* Counts all element from the cache matching the given key prefix.
* <p>NOTE: Typically one should suffix prefixes with {@link #SEPARATOR} (not added).</p>
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.1.0: Added.
*/
public int countByKeyPrefix(String keyPrefix) {
return countByFilter(new KeyPrefixCacheEntryFilter<>(keyPrefix));
}
/**
* Removes an element from the cache according to the specified key
* @param key The key for the element, used to reference it in the hashtables and LRU linked list
* @return The value of the removed element specified by the key
*/
public V remove(Object key) {
return this.removeInternal(key, true);
}
/**
* Remove operation; only use if existing cache line not available (see {@link #removeInternal(Object, CacheLine, boolean)}).
* <p>SCIPIO: 2.1.0: Removed synchronized because only used on remove calls and ConcurrentHashMap can usually handle bulk removal iterations without it.</p>
* <p>SCIPIO: 2.1.0: Added removeRelated for shortKey removal.</p>
* @see #removeInternal(Object, CacheLine, boolean)
*/
protected V removeInternal(Object key, boolean explicit) { // SCIPIO: 2.1.0: Removed synchronized
CacheLine<V> oldCacheLine = memoryTable.remove(fromKey(key));
V oldValue = (oldCacheLine != null) ? oldCacheLine.getValue() : null;
if (oldCacheLine != null) {
if (!indexList.isEmpty()) { // SCIPIO: 2.1.0: Added for UtilCache.Index support
// SCIPIO: NOTE: We only remove if the memoryTable.remove() hit succeeded to guarantee we only do
// a single remove call for this cache line per index. Because putInternal() adds cache lines to indexes
// before the main memoryTable, cache lines are visible early in the indexes to operations such as
// removeByIndexKey(). So by removing from indexes only in the case the memoryTable.remove() actually
// succeeds, it ensures 1) at most one remove call per index and 2) no index removes can happen before and
// until the cache line is added to the main memoryTable. Note that we must also cover the case where
// removal happens some other way, such as ConcurrentLinkedHashMap maxInMemory #onEviction() callback.
removeCacheLineFromIndexes(oldCacheLine);
}
cancel(oldCacheLine);
}
if (oldValue != null) {
noteRemoval(UtilGenerics.cast(key), oldValue);
if (explicit) {
removeHitCount.incrementAndGet();
}
return oldValue;
}
if (explicit) {
removeMissCount.incrementAndGet();
}
return null;
}
/**
* Remove operation; if existingCacheLine non-null, only removes key if matching existing cache line.
* <p>SCIPIO: 2.1.0: Removed synchronized because only used on remove calls and ConcurrentHashMap can usually handle bulk removal iterations without it.</p>
* <p>SCIPIO: 2.1.0: cancel() call now occurs after memoryTable remove for consistency between overloads and double-cancel prevention.</p>
*/
protected boolean removeInternal(Object key, CacheLine<V> existingCacheLine, boolean explicit) { // SCIPIO: 2.1.0: Removed synchronized, added explicit
//cancel(existingCacheLine); // SCIPIO: 2.1.0: Moved below so that only called once actually removed
if (memoryTable.remove(fromKey(key), existingCacheLine)) {
if (!indexList.isEmpty()) { // SCIPIO: 2.1.0: Added for UtilCache.Index support
// SCIPIO: NOTE: We only remove if the memoryTable.remove() hit succeeded to guarantee we only do
// a single remove call for this cache line per index. Because putInternal() adds cache lines to indexes
// before the main memoryTable, cache lines are visible early in the indexes to operations such as
// removeByIndexKey(). So by removing from indexes only in the case the memoryTable.remove() actually
// succeeds, it ensures 1) at most one remove call per index and 2) no index removes can happen before and
// until the cache line is added to the main memoryTable. Note that we must also cover the case where
// removal happens some other way, such as ConcurrentLinkedHashMap maxInMemory #onEviction() callback.
removeCacheLineFromIndexes(existingCacheLine);
}
V oldValue = existingCacheLine.getValue();
cancel(existingCacheLine);
if (oldValue != null) {
noteRemoval(UtilGenerics.cast(key), oldValue);
if (explicit) {
removeHitCount.incrementAndGet();
}
return true;
}
}
if (explicit) {
removeMissCount.incrementAndGet();
}
return false;
}
/**
* Removes all elements from this cache.
* <p>SCIPIO: NOTE: This method will retain synchronized(UtilCache.this) </p>
* <p>SCIPIO: 2.1.0: Replaced implementation because <code>it.remove()</code> is key-based without specific value check.</p>
* <p>SCIPIO: 2.1.0: Now synchronizes on dedicated eraseLockObj for performance reasons; only {@link #erase()} should lock on it.</p>
*/
public int erase() {
int removed = 0;
synchronized(eraseLockObj) { // SCIPIO: 2.1.0: Added eraseLockObj
for (Map.Entry<Object, CacheLine<V>> entry : memoryTable.entrySet()) {
if (removeInternal(entry.getKey(), entry.getValue(), true)) {
removed++;
}
}
}
return removed;
}
/**
* Removes all element from the cache matching the given filter.
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.0.0: Added.</p>
* <p>SCIPIO: 2.1.0: Removed synchronized because only used on remove calls and ConcurrentHashMap can usually
* handle bulk removal iterations without it.</p>
* @param entryFilter The entry filter - return true to remove key
*/
public int removeByFilter(CacheEntryFilter<K, V> entryFilter) { // SCIPIO: 2.1.0: Removed synchronized
int removed = 0;
for (Map.Entry<Object, CacheLine<V>> entry : memoryTable.entrySet()) {
if (entryFilter.filter(UtilGenerics.cast(toKey(entry.getKey())), UtilGenerics.cast(entry.getValue().getValue()))) {
if (removeInternal(entry.getKey(), entry.getValue(), true)) {
removed++;
}
}
}
return removed;
}
/**
* Removes all cache entries linked to the given index key.
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public void removeByIndexKey(String indexName, Object indexKey) {
Index index = indexNameMap.get(indexName);
if (index == null) {
if (indexDefs.get(indexName) == null) {
throw new IllegalArgumentException("Invalid index name [" + indexName + "] for cache [" + getName() + "]");
} else {
return;
}
}
index.removeIndexLineMainKeysFromCache(indexKey);
}
/**
* Removes all element from the cache matching the given key regex pattern (uses {@link java.util.regex.Matcher#matches()} on each key).
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.1.0: Added.
*/
public int removeByKeyPat(Pattern keyPat) {
return removeByFilter(new KeyPatternCacheEntryFilter<>(keyPat));
}
/**
* Removes all element from the cache matching the given key regex pattern (uses {@link java.util.regex.Matcher#matches()} on each key).
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.1.0: Added.
*/
public int removeByKeyPat(String keyPat) {
return removeByFilter(new KeyPatternCacheEntryFilter<>(keyPat));
}
/**
* Removes all element from the cache matching the given key prefix.
* <p>NOTE: Typically one should suffix prefixes with {@link #SEPARATOR} (not added).</p>
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.1.0: Added.
*/
public int removeByKeyPrefix(String keyPrefix) {
return removeByFilter(new KeyPrefixCacheEntryFilter<>(keyPrefix));
}
/**
* Removes all element from the cache having the given key substring.
* <p>NOTE: Typically one should suffix prefixes with {@link #SEPARATOR} (not added).</p>
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.1.0: Added.
*/
public int removeByKeySubstring(String keySub) {
return removeByFilter(new KeySubstringCacheEntryFilter<>(keySub));
}
/**
* Removes all element from the cache matching the given key parts (pre-separated).
* <p>null and empty string key parts are treated as wildcards matching any value in the key at that position.</p>
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.1.0: Added.
*/
public int removeByKeyParts(Collection<String> keyParts) {
return removeByFilter(new KeyPartsCacheEntryFilter<>(keyParts));
}
/**
* Removes all element from the cache matching the given key parts (pre-separated).
* <p>null and empty string key parts are treated as wildcards matching any value in the key at that position.</p>
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.1.0: Added.
*/
public int removeByKeyParts(String... keyParts) {
return removeByFilter(new KeyPartsCacheEntryFilter<>(keyParts));
}
/**
* Removes all element from the cache matching the given key part string delimited by {@link #SEPARATOR}.
* <p>null and empty string key parts are treated as wildcards matching any value in the key at that position.</p>
* <p>e.g.: <code>"default::*::PH-1000"</code> (<code>delegator::productStoreId::productId::featureId</code>)</p>
* <p>WARN: Slow on large caches.</p>
* <p>SCIPIO: 2.1.0: Added.
*/
public int removeByKeyPartString(String key) {
return removeByFilter(new KeyPartsCacheEntryFilter<>(splitKey(key)));
}
/**
* Cache entry filter.
* <p>SCIPIO: 2.0.0: Added.</p>
* @see #removeByFilter(CacheEntryFilter)
*/
public interface CacheEntryFilter<K, V> {
boolean filter(K key, V value);
}
/**
* Negating cache entry filter.
* <p>SCIPIO: 2.0.0: Added.</p>
* @see #removeByFilter(CacheEntryFilter)
*/
public static class NotCacheEntryFilter<K, V> implements CacheEntryFilter<K, V> {
private final CacheEntryFilter<K, V> negatedFilter;
public NotCacheEntryFilter(CacheEntryFilter<K, V> negatedFilter) {
this.negatedFilter = negatedFilter;
}
public CacheEntryFilter<K, V> getNegatedFilter() {
return negatedFilter;
}
@Override
public boolean filter(K key, V value) {
return !getNegatedFilter().filter(key, value);
}
}
/**
* Key pattern regex filter.
* <p>SCIPIO: 2.1.0: Added.</p>
* @see #removeByFilter(CacheEntryFilter)}
*/
public static class KeyPatternCacheEntryFilter<K, V> implements CacheEntryFilter<K, V> {
private final Pattern keyPat;
public KeyPatternCacheEntryFilter(Pattern keyPat) {
this.keyPat = keyPat;
}
public KeyPatternCacheEntryFilter(String keyPat) {
this(Pattern.compile(keyPat));
}
protected Pattern getKeyPat() {
return keyPat;
}
@Override
public boolean filter(K key, V value) {
return (key != null && key != ObjectType.NULL && getKeyPat().matcher(key.toString()).matches());
}
}
/**
* Key prefix filter.
* <p>NOTE: Typically one should suffix prefixes with {@link #SEPARATOR} (not added).</p>
* <p>SCIPIO: 2.1.0: Added.</p>
* @see #removeByFilter(CacheEntryFilter)}
* @see String#startsWith(String)
*/
public static class KeyPrefixCacheEntryFilter<K, V> implements CacheEntryFilter<K, V> {
private final String keyPrefix;
public KeyPrefixCacheEntryFilter(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
protected String getKeyPrefix() {
return keyPrefix;
}
@Override
public boolean filter(K key, V value) {
return (key != null && key != ObjectType.NULL && key.toString().startsWith(getKeyPrefix()));
}
}
/**
* Key substring filter.
* <p>NOTE: Typically one should prefix and/or suffix prefixes with {@link #SEPARATOR} (not added).</p>
* <p>SCIPIO: 2.1.0: Added.</p>
* @see #removeByFilter(CacheEntryFilter)}
* @see String#startsWith(String)
*/
public static class KeySubstringCacheEntryFilter<K, V> implements CacheEntryFilter<K, V> {
private final String keySub;
public KeySubstringCacheEntryFilter(String keySub) {
this.keySub = keySub;
}
protected String getKeySub() {
return keySub;
}
@Override
public boolean filter(K key, V value) {
return (key != null && key != ObjectType.NULL && key.toString().contains(getKeySub()));
}
}
/**
* Key parts filter, where each string in the key parts array corresponds to a delimited part of the target key,
* or accepts any value if null.
* <p>SCIPIO: 2.1.0: Added.</p>
* @see #removeByFilter(CacheEntryFilter)}
*/
public static class KeyPartsCacheEntryFilter<K, V> implements CacheEntryFilter<K, V> {
private final List<String> keyParts;
public KeyPartsCacheEntryFilter(Collection<String> keyParts) {
this.keyParts = keyParts.stream()
.map(p -> (p != null && !p.isEmpty()) ? p : null)
.collect(Collectors.toCollection(ArrayList::new));
}
public KeyPartsCacheEntryFilter(String... keyParts) {
this(Arrays.asList(keyParts));
}
protected List<String> getKeyParts() {
return keyParts;
}
@Override
public boolean filter(K key, V value) {
List<String> keyParts = getKeyParts();
if (keyParts.isEmpty()) {
return true; // Accept all (wilcard by default)
}
if (key == null || key == ObjectType.NULL) {
return false;
}
List<String> splitParts = StringUtil.split(null, key.toString(), SEPARATOR);
if (splitParts == null) {
return keyParts.isEmpty();
}
for(int i = 0; i < keyParts.size(); i++) {
String keyPart = keyParts.get(i);
if (keyPart != null && (i >= splitParts.size() || !keyPart.equals(splitParts.get(i)))) {
return false;
}
}
return true;
}
}
/**
* Removes all elements from this cache and clears counters.
*/
public int clear() {
int removed = erase();
clearCounters();
return removed;
}
public static void clearAllCaches() {
clearAllCaches(null, null);
}
/**
* Clears all caches with support for excludes.
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public static void clearAllCaches(Collection<String> excludeNames, Collection<Pattern> excludePatterns) {
// We make a copy since clear may take time
for (UtilCache<?,?> cache : utilCacheTable.values()) {
if (excludeNames != null && excludeNames.contains(cache.getName())) {
continue;
} else if (excludePatterns != null) {
boolean excluded = false;
for(Pattern pat : excludePatterns) {
if (pat.matcher(cache.getName()).matches()) {
excluded = true;
break;
}
}
if (excluded) {
continue;
}
}
cache.clear();
}
}
public static Set<String> getUtilCacheTableKeySet() {
Set<String> set = new HashSet<>(utilCacheTable.size());
set.addAll(utilCacheTable.keySet());
return set;
}
/**
* Getter for the name of the UtilCache instance.
* @return The name of the instance
*/
public String getName() {
return this.name;
}
/**
* Getter for the simple name (without sequence index number) of the UtilCache instance.
* <p>SCIPIO: 2.1.0: Added.</p>
* @return The simple name of the instance
*/
public String getBaseName() {
return this.baseName;
}
/**
* True if cache is enabled or false if disabled, in which case {@link #put} or {@link #putIfAbsent} have no effect
* and {@link #get(Object)} operations always return null.
* <p>Can be set false using explicit "enabled=false" cache property or {@link #setEnabled(boolean)}, and supports
* real-time toggling only insofar as this flag is not <code>volatile</code>, meaning the jvm publishes the toggling
* lazily to other threads with no time guarantee.</p>
* <p>SCIPIO: 2.0.0: Added.</p>
*/
public boolean isEnabled() {
return enabled;
}
/**
* Sets cache enabled or disabled, in which case {@link #put} or {@link #putIfAbsent} have no effect
* and {@link #get(Object)} operations always return null.
* <p>Can be set false using explicit "enabled=false" cache property or {@link #setEnabled(boolean)}, and supports
* real-time toggling only insofar as this flag is not <code>volatile</code>, meaning the jvm publishes the toggling
* lazily to other threads with no time guarantee.</p>
* <p>SCIPIO: 2.0.0: Added.</p>
*/
public UtilCache<K, V> setEnabled(boolean enabled) {
this.enabled = enabled;
return this;
}
/**
* Returns the number of successful hits on the cache.
* @return The number of successful cache hits
*/
public long getHitCount() {
return this.hitCount.get();
}
/**
* Returns the number of cache misses from entries that are not found in the cache.
* @return The number of cache misses
*/
public long getMissCountNotFound() {
return this.missCountNotFound.get();
}
/**
* Returns the number of cache misses from entries that are expired.
* @return The number of cache misses
*/
public long getMissCountExpired() {
return this.missCountExpired.get();
}
/**
* Returns the number of cache misses from entries that are have had the soft reference cleared out (by garbage collector and such).
* @return The number of cache misses
*/
public long getMissCountSoftRef() {
return this.missCountSoftRef.get();
}
/**
* Returns the number of cache misses caused by any reason.
* @return The number of cache misses
*/
public long getMissCountTotal() {
return getMissCountSoftRef() + getMissCountNotFound() + getMissCountExpired();
}
public long getRemoveHitCount() {
return this.removeHitCount.get();
}
public long getRemoveMissCount() {
return this.removeMissCount.get();
}
/** Clears the hit and miss counters. */
public void clearCounters() {
this.hitCount.set(0);
this.missCountNotFound.set(0);
this.missCountExpired.set(0);
this.missCountSoftRef.set(0);
this.removeHitCount.set(0);
this.removeMissCount.set(0);
}
public void setMaxInMemory(int newInMemory) {
this.maxInMemory = newInMemory;
Map<Object, CacheLine<V>> oldmap = this.memoryTable;
if (newInMemory > 0) {
if (this.memoryTable instanceof ConcurrentLinkedHashMap<?, ?>) {
((ConcurrentLinkedHashMap<?, ?>) this.memoryTable).setCapacity(newInMemory);
return;
}
this.memoryTable = new ConcurrentLinkedHashMap.Builder<Object, CacheLine<V>>()
.maximumWeightedCapacity(newInMemory)
.build();
} else {
this.memoryTable = new ConcurrentHashMap<>();
}
this.memoryTable.putAll(oldmap);
}
public int getMaxInMemory() {
return maxInMemory;
}
public void setSizeLimit(int newSizeLimit) {
this.sizeLimit = newSizeLimit;
}
public int getSizeLimit() {
return sizeLimit;
}
/**
* Sets the expire delay for the cache elements, in milliseconds.
* <p>If 0, elements never expire.</p>
* <p>SCIPIO: 2.1.0: Corrected time -> delay, but neither of these methods are recommended anymore since
* {@link Builder} is available.</p>
* @param expireDelayMillis The expire time for the cache elements, in milliseconds
*/
public void setExpireDelay(long expireDelayMillis) {
// if expire time was <= 0 and is now greater, fill expire table now
if (expireDelayMillis > 0) {
long expireDelayNanos = TimeUnit.NANOSECONDS.convert(expireDelayMillis, TimeUnit.MILLISECONDS);
this.expireDelayNanos = expireDelayNanos;
for (Map.Entry<?, CacheLine<V>> entry: memoryTable.entrySet()) {
// SCIPIO: 2.1.0: Defer pulse registration following memoryTable update
CacheLine<V> newCacheLine = entry.getValue().changeLine(useSoftReference, expireDelayNanos);
entry.setValue(newCacheLine);
registerExpirePulse(newCacheLine);
}
} else {
this.expireDelayNanos = 0;
// if expire time was > 0 and is now <=, do nothing, just leave the load times in place, won't hurt anything...
}
}
/**
* Return the current expire time for the cache elements, in nanoseconds (approximated).
* <p>SCIPIO: 2.1.0: Added.</p>
* @return The expire time for the cache elements, in nanoseconds.
*/
public long getExpireDelayNanos() {
return expireDelayNanos;
}
/**
* Return the current expire time for the cache elements, in milliseconds.
* @return The expire time for the cache elements, in milliseconds.
*/
public long getExpireDelay() {
return TimeUnit.MILLISECONDS.convert(expireDelayNanos, TimeUnit.NANOSECONDS);
}
/**
* Sets the expire delay for the cache elements, in milliseconds.
* <p>If 0, elements never expire.</p>
* <p>SCIPIO: 2.1.0: The correct method to use is {@link #setExpireDelay(long)}, but neither of these methods
* are recommended anymore since {@link Builder} is available.</p>
* @param expireDelayMillis The expire time for the cache elements, in milliseconds
* @see #setExpireDelay(long)
*/
public void setExpireTime(long expireDelayMillis) {
setExpireDelay(expireDelayMillis);
}
/**
* Return the current expire time for the cache elements.
* @return The expire time for the cache elements
* @see #getExpireDelay()
*/
public long getExpireTime() {
return getExpireDelay();
}
/** Set whether or not the cache lines should use a soft reference to the data */
public void setUseSoftReference(boolean useSoftReference) {
if (this.useSoftReference != useSoftReference) {
this.useSoftReference = useSoftReference;
for (Map.Entry<?, CacheLine<V>> entry: memoryTable.entrySet()) {
// SCIPIO: 2.1.0: Defer pulse registration following memoryTable update
CacheLine<V> newCacheLine = entry.getValue().changeLine(useSoftReference, expireDelayNanos);
entry.setValue(newCacheLine);
registerExpirePulse(newCacheLine);
}
}
}
/** Return whether or not the cache lines should use a soft reference to the data */
public boolean getUseSoftReference() {
return this.useSoftReference;
}
/**
* Returns the number of elements currently in the cache
* @return The number of elements currently in the cache
*/
public int size() {
return memoryTable.size();
}
/**
* Returns a boolean specifying whether or not an element with the specified key is in the cache.
* @param key The key for the element, used to reference it in the hashtables and LRU linked list
* @return True is the cache contains an element corresponding to the specified key, otherwise false
*/
public boolean containsKey(Object key) {
return (memoryTable.get(fromKey(key)) != null);
}
/**
* Gets cache line keys.
* <p>NOTE: this returns an unmodifiable copy of the keySet, so removing from here won't have an effect,
* and calling a remove while iterating through the set will not cause a concurrent modification exception.
* This behavior is necessary for now for the persisted cache feature.</p>
*/
public Set<? extends K> getCacheLineKeys() {
// note that this must be a HashSet and not a FastSet in order to have a null value
Set<Object> keys;
if (memoryTable.containsKey(ObjectType.NULL)) {
keys = new HashSet<>(memoryTable.keySet());
keys.remove(ObjectType.NULL);
keys.add(null);
} else {
keys = memoryTable.keySet();
}
return Collections.unmodifiableSet(UtilGenerics.<Set<? extends K>>cast(keys));
}
public Collection<? extends CacheLine<V>> getCacheLineValues() {
throw new UnsupportedOperationException();
}
private Map<String, Object> createLineInfo(int keyNum, K key, CacheLine<V> line) {
Map<String, Object> lineInfo = new LinkedHashMap<>();
lineInfo.put("elementKey", key);
if (line.getLoadTimeNanos() > 0) {
lineInfo.put("expireTimeMillis", TimeUnit.MILLISECONDS.convert(line.getExpireTimeNanos() - System.nanoTime(), TimeUnit.NANOSECONDS));
}
lineInfo.put("lineSize", findSizeInBytes(line.getValue(), key)); // SCIPIO: pass key
lineInfo.put("keyNum", keyNum);
return lineInfo;
}
public Collection<? extends Map<String, Object>> getLineInfos() {
Set<? extends K> cacheLineKeys = getCacheLineKeys();
List<Map<String, Object>> lineInfos = new ArrayList<>(cacheLineKeys.size());
int keyIndex = 0;
for (K key : cacheLineKeys) {
CacheLine<V> line = memoryTable.get(fromKey(key));
if (line != null) {
lineInfos.add(createLineInfo(keyIndex, key, line));
}
keyIndex++;
}
return lineInfos;
}
/** Send a key addition event to all registered listeners */
protected void noteAddition(K key, V newValue) {
Collection<CacheListener<K, V>> listeners = this.listeners;
for (CacheListener<K, V> listener : listeners) {
listener.noteKeyAddition(this, key, newValue);
}
}
/** Send a key removal event to all registered listeners */
protected void noteRemoval(K key, V oldValue) {
Collection<CacheListener<K, V>> listeners = this.listeners;
for (CacheListener<K, V> listener : listeners) {
listener.noteKeyRemoval(this, key, oldValue);
}
}
/** Send a key update event to all registered listeners */
protected void noteUpdate(K key, V newValue, V oldValue) {
Collection<CacheListener<K, V>> listeners = this.listeners;
for (CacheListener<K, V> listener : listeners) {
listener.noteKeyUpdate(this, key, newValue, oldValue);
}
}
/**
* Adds an event listener for key removals.
* <p>SCIPIO: 2.1.0: NOTE: Do not use cache listeners except best-effort stats; cache modifications not synchronized.</p>
* <p>SCIPIO: 2.1.0: Added synchronization and optimized.</p>
*/
public synchronized UtilCache<K, V> addListener(CacheListener<K, V> listener) {
Collection<CacheListener<K, V>> listeners = this.listeners;
if (listeners.isEmpty()) {
listeners = new CopyOnWriteArraySet<>();
listeners.add(listener);
this.listeners = listeners;
} else {
listeners.add(listener);
}
return this;
}
/**
* Removes an event listener for key removals.
* <p>SCIPIO: 2.1.0: NOTE: Do not use cache listeners except best-effort stats; cache modifications not synchronized.</p>
* <p>SCIPIO: 2.1.0: Added synchronization and optimized.</p>
*/
public synchronized UtilCache<K, V> removeListener(CacheListener<K, V> listener) {
Collection<CacheListener<K, V>> listeners = this.listeners;
if (!listeners.isEmpty()) {
listeners.remove(listener);
if (listeners.isEmpty()) {
this.listeners = Collections.emptySet();
}
}
return this;
}
/** Checks for a non-expired key in a specific cache */
public static boolean validKey(String cacheName, Object key) {
UtilCache<?, ?> cache = findCache(cacheName);
if (cache != null) {
if (cache.containsKey(key)) {
return true;
}
}
return false;
}
/**
* Splits key parts from a separator-delimited key.
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public static <C extends Collection<String>> C splitKey(C out, String key) {
return StringUtil.split(out, key, SEPARATOR);
}
/**
* Splits key parts from a separator-delimited key.
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public static List<String> splitKey(String key) {
return splitKey(new ArrayList<>(), key);
}
public static Object getCacheElement(String cacheName, String key) {
try{
UtilCache<?, ?> cacheTable = utilCacheTable.get(cacheName);
if(UtilValidate.isNotEmpty(cacheTable)){
return cacheTable.get(key);
}
}catch(Exception e){
}
return null;
}
public static void clearCachesThatStartWith(String cacheNamePrefix) {
for (Map.Entry<String, UtilCache<?, ?>> entry : utilCacheTable.entrySet()) {
String name = entry.getKey();
if (name.startsWith(cacheNamePrefix)) {
entry.getValue().clear();
}
}
}
public static void clearKeysThatStartWithFromCache(String cacheName, String keyPrefix) {
try {
UtilCache<?, ?> cacheObj = utilCacheTable.get(cacheName);
if (cacheObj != null) {
cacheObj.removeByKeyPrefix(keyPrefix);
}
} catch(Exception e) {
Debug.logWarning("Could not find or clear caches from cache [" + cacheName + "]", module);
}
}
public static void clearKeysThatContainFromCache(String cacheName, String keySub) {
try{
UtilCache<?, ?> cacheObj = utilCacheTable.get(cacheName);
if (cacheObj != null) {
cacheObj.removeByKeySubstring(keySub);
}
} catch(Exception e) {
Debug.logWarning("Could not find or clear caches from cache "+cacheName,module);
}
}
public static void clearCache(String cacheName) {
UtilCache<?, ?> cache = findCache(cacheName);
if (cache == null) {
return;
}
cache.clear();
}
/**
* Removal of individual cache objects by key
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public void clearCacheValue(String cacheName,String key) {
UtilCache<?, ?> cache = findCache(cacheName);
if (cache == null) {
return;
}
try {
cache.remove(key);
} catch(Exception e) {
// TODO: review
}
}
/**
* Outputs descriptors to a map.
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public static <M extends Map<String, Map<String, Object>>> M getDescriptors(M out, Collection<String> types) {
for(Map.Entry<String, UtilCache<?, ?>> entry : (out instanceof TreeMap ? utilCacheTable.entrySet() : new TreeSet<>(utilCacheTable.entrySet()))) {
out.put(entry.getKey(), entry.getValue().getDescriptor(types));
}
return out;
}
/**
* Outputs descriptors to a sorted map copy.
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public static Map<String, Map<String, Object>> getDescriptors(Collection<String> types) {
return getDescriptors(new TreeMap<>(), types);
}
/**
* Outputs descriptor fields to the given map
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public <M extends Map<String, Object>> M getDescriptor(M out, Collection<String> types) { // SCIPIO: 2.1.0: Added for convenience/conversion
if (types == null || types.contains("id")) {
//out.put("baseName", this.getBaseName());
out.put("name", this.getName());
//out.put("cacheName", this.getName());
}
if (types == null || types.contains("options")) {
out.put("enabled", this.isEnabled());
out.put("sizeLimit", this.getSizeLimit());
out.put("maxInMemory", this.getMaxInMemory());
out.put("expireDelay", this.getExpireDelay());
out.put("expireDelayNanos", this.expireDelayNanos);
out.put("useSoftReference", this.getUseSoftReference());
//out.put("keyFormat", this.keyFormat);
//out.put("indexDefs", this.indexDefs);
}
if (types == null || types.contains("stats-basic")) {
out.put("size", this.size());
//out.put("cacheSize", this.size());
}
if (types == null || types.contains("stats-hit")) {
out.put("hitCount", this.getHitCount());
out.put("missCountTotal", this.getMissCountTotal());
out.put("missCountNotFound", this.getMissCountNotFound());
out.put("missCountExpired", this.getMissCountExpired());
out.put("missCountSoftRef", this.getMissCountSoftRef());
out.put("removeHitCount", this.getRemoveHitCount());
out.put("removeMissCount", this.getRemoveMissCount());
}
if (types == null || types.contains("stats-mem")) {
out.put("cacheMemory", this.getSizeInBytes());
}
return out;
}
/**
* Outputs descriptor fields to a map.
* <p>SCIPIO: 2.1.0: Added.</p>
*/
public Map<String, Object> getDescriptor(Collection<String> types) {
return getDescriptor(new LinkedHashMap<>(), types);
}
/**
* Makes a map of indexes by index name describing for each index a keyFormat derived from the supplied memoryTable
* entry full keyFormat.
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support.</p>
*/
protected Map<String, Index> makeIndexes(Map<String, ?> indexDefs) {
Map<String, Index> indexTables = new LinkedHashMap<>();
for(Map.Entry<String, ?> entry : indexDefs.entrySet()) {
String indexName = entry.getKey();
String indexKeyFormat = null;
if (entry.getValue() instanceof String) {
indexKeyFormat = (String) entry.getValue();
} else if (entry.getValue() instanceof Map) {
Map<String, ?> values = UtilGenerics.cast(entry.getValue());
indexKeyFormat = (String) values.get("keyFormat");
boolean enabled = UtilMisc.booleanValue(values.get("enabled"), true);
if (!enabled) {
Debug.logInfo("Cache [" + getName() + "] index [" + indexName + "] disabled; skipping registration", module);
continue;
}
}
if (UtilValidate.isEmpty(indexKeyFormat)) {
Debug.logWarning("Cache [" + getName() + "] index [" + indexName + "] does not define a keyFormat; treating as disabled and skipping registration", module);
continue;
}
Index index = makeIndex(indexName, indexKeyFormat, indexDefs);
indexTables.put(indexName, index);
}
return indexTables;
}
protected Index getIndex(String indexName) {
return indexNameMap.get(indexName);
}
protected void addCacheLineToIndexes(CacheLine<V> cacheLine) { // SCIPIO: 2.1.0: Added
for(Index index : indexList) {
index.addCacheLine(cacheLine);
}
}
protected void removeCacheLineFromIndexes(CacheLine<V> cacheLine) { // SCIPIO: 2.1.0: Added
for(Index index : indexList) {
index.removeCacheLine(cacheLine);
}
}
protected Index makeIndex(String indexName, String indexKeyFormat, Map<String, ?> indexDefs) {
return new Index(indexName, indexKeyFormat, indexDefs);
}
/**
* Maps index keys to sets of cache lines.
* <p>Configured via cache.properties or via {@link Builder}.</p>
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support.</p>
*/
protected final class Index implements Serializable {
protected final String indexName;
protected final String indexKeyFormat;
protected final List<Integer> indexKeyOffsets;
/**
* Map of index keys to index lines of cache lines.
* <p>NOTE: As soon as an index line goes empty, it is considered expired and expected to be removed
* (non-trivial for fast synchronization).</p>
*/
protected final ConcurrentMap<Object, IndexLine> indexLines;
/**
* Map of index keys to index lines of cache lines.
* <p>NOTE: As soon as an index line goes empty, it is considered expired and expected to be removed
* (non-trivial for fast synchronization).</p>
*/
protected final ConcurrentMap<CacheLine<V>, IndexNode> indexNodes;
protected Index(String indexName, String indexKeyFormat, Map<String, ?> indexDefs) {
if (UtilValidate.isEmpty(name)) {
throw new IllegalArgumentException("missing index name");
}
if (UtilValidate.isEmpty(indexKeyFormat)) {
throw new IllegalArgumentException("missing index keyFormat");
}
this.indexName = indexName;
this.indexKeyFormat = indexKeyFormat;
if (indexKeyFormat.startsWith("[") && indexKeyFormat.endsWith("]")) {
List<Integer> newIndexKeyOffsets = Arrays.stream(indexKeyFormat.substring(1, indexKeyFormat.length() - 1).split(","))
.map(Integer::parseInt).collect(Collectors.toList());
this.indexKeyOffsets = newIndexKeyOffsets;
} else {
String[] splitKeyFormat = UtilCache.this.keyFormat.split(SEPARATOR);
String[] splitIndexKeyFormat = this.indexKeyFormat.split(SEPARATOR);
Collection<?> indexKeyOffsets = extractIndexKeyOffsets(splitIndexKeyFormat, splitKeyFormat);
if (UtilValidate.isEmpty(indexKeyOffsets)) {
throw new IllegalArgumentException("invalid index keyFormat");
}
List<Integer> newIndexKeyOffsets = new ArrayList<>(indexKeyOffsets.size());
for (Object indexKeyOffsetObj : indexKeyOffsets) {
Integer indexKeyOffset = UtilMisc.toInteger(indexKeyOffsetObj, null);
if (indexKeyOffset == null || indexKeyOffset < 0 || indexKeyOffset >= splitKeyFormat.length) {
throw new IllegalArgumentException("invalid index keyFormat");
}
newIndexKeyOffsets.add(indexKeyOffset);
}
UtilGenerics.<ArrayList<?>>cast(newIndexKeyOffsets).trimToSize();
this.indexKeyOffsets = newIndexKeyOffsets;
}
this.indexLines = new ConcurrentHashMap<>();
this.indexNodes = new ConcurrentHashMap<>();
}
public String getIndexName() {
return indexName;
}
public String getIndexKeyFormat() {
return indexKeyFormat;
}
public Object getIndexKey(CacheLine<?> cacheLine) {
return extractIndexKey(cacheLine.getKey());
}
public Object extractIndexKey(Object fullKey) {
return UtilCache.this.extractIndexKey(fullKey.toString(), indexKeyOffsets);
}
/**
* Adds a cache line to the index (only).
* <p>Contract: At the end of this call, the cache is guaranteed to be in the index line and the index line
* is checked to ensure it is still the active one before the call finishes. This is only correct and valid
* as this method must only be called <i>before</i> the cacheline is added to the memoryTable; no other
* use of this method is valid.</p>
*/
protected void addCacheLine(CacheLine<V> cacheLine) {
Object indexKey = getIndexKey(cacheLine);
if (indexKey == null) {
return;
}
while (true) {
IndexLine indexLine = indexLines.get(indexKey);
if (indexLine == null) {
indexLine = new IndexLine(new IndexNode(cacheLine));
IndexLine prevIndexLine = indexLines.putIfAbsent(indexKey, indexLine);
if (prevIndexLine == null) {
return; // Successfully added new index line
} else {
indexLine = prevIndexLine;
}
}
// TODO: Optimize: synchronized can probably be more fine-grained, but difficult due to linked list
synchronized (indexLine) {
// Add as first instead of last so that iterators/loops that start at an index node won't go over newly-added nodes
IndexNode firstIndexNode = indexLine.first;
if (firstIndexNode == null) {
// Don't add to empty index line, always remove and create new line, by convention
indexLines.remove(indexKey, indexLine);
continue;
}
IndexNode indexNode = new IndexNode(cacheLine, null, firstIndexNode);
firstIndexNode.previous = indexNode;
indexLine.first = indexNode;
indexNodes.put(cacheLine, indexNode);
return;
}
}
}
/**
* Removes cache line from the index (only).
*/
protected void removeCacheLine(CacheLine<V> cacheLine) {
Object indexKey = getIndexKey(cacheLine);
if (indexKey == null) {
return;
}
IndexLine indexLine = indexLines.get(indexKey);
if (indexLine == null) {
return;
}
// TODO: Optimize: synchronized can probably be more fine-grained, but difficult due to linked list
synchronized (indexLine) {
IndexNode indexNode = indexNodes.remove(cacheLine);
if (indexNode != null) {
IndexNode prevIndexNode = indexNode.previous;
IndexNode nextIndexNode = indexNode.next;
if (prevIndexNode != null) {
prevIndexNode.next = indexNode.next;
}
if (nextIndexNode != null) {
nextIndexNode.previous = indexNode.previous;
}
if (indexLine.first == indexNode) {
indexLine.first = indexNode.next;
}
//if (indexLine.last == indexNode) {
// indexLine.last = indexNode.previous;
//}
}
if (indexLine.first == null) {
indexLines.remove(indexKey, indexLine);
}
}
}
/**
* Performs a general cache remove on all keys recorded in this index line.
* <p>NOTE: Not fully synchronized for speed reasons.</p>
*/
protected void removeIndexLineMainKeysFromCache(Object indexKey) {
IndexLine indexLine = this.indexLines.remove(indexKey);
if (indexLine == null) {
return;
}
IndexNode indexNode;
synchronized (indexLine) {
indexNode = indexLine.first;
}
// NOTE: Not using synchronized block because it should be unnecessary as long as the node next references are never nulled,
// the removals are guarded against multiple removals by ConcurrentMap.remove(Object, Object) in removeInternal(),
// and the addCacheLine() calls add at head/first instead of at tail/last so they don't affect this iteration.
while(indexNode != null) {
CacheLine<V> cacheLine = UtilGenerics.cast(indexNode.cacheLine);
removeInternal(cacheLine.getKey(), cacheLine, true);
indexNode = indexNode.next;
}
}
}
/**
* An index line, one per index key, container cache lines.
* <p>Considered expired and removed from the index table as soon as empty.</p>
* <p>NOTE: This was originally an encapsulated inner class of Index with synchronized(this), but to lower memory
* usage and overhead a trivial wrapper is preferable; Index synchronizes on IndexLine instances and implements
* all methods itself to retain access to UtilCache and Index members.</o>
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support.</p>
*/
private final static class IndexLine implements Serializable {
protected volatile IndexNode first;
//protected volatile IndexNode last; // not currently needed
// Not needed
//protected volatile int size;
protected final ReentrantLock removeLock = new ReentrantLock();
IndexLine(IndexNode initialNode) {
this.first = initialNode;
//this.last = initialNode;
}
}
private final static class IndexNode implements Serializable {
private final CacheLine<?> cacheLine;
private volatile IndexNode previous;
private volatile IndexNode next;
IndexNode(CacheLine<?> cacheLine, IndexNode previous, IndexNode next) {
this.cacheLine = cacheLine;
this.previous = previous;
this.next = next;
}
IndexNode(CacheLine<?> cacheLine) {
this(cacheLine, null, null);
}
}
/**
* Extracts an index key using an array of key part positions in a full key for this cache.
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support.</p>
*/
protected String extractIndexKey(String fullKey, List<Integer> indexKeyOffsets) {
return extractIndexKey(fullKey, indexKeyOffsets, SEPARATOR);
}
/**
* Extracts an index key using an array of key part positions in a full key for this cache.
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support.</p>
*/
protected static String extractIndexKey(String fullKey, List<Integer> indexKeyOffsets, String delim) {
StringBuilder indexKey = new StringBuilder();
for(int indexKeyOffset : indexKeyOffsets) {
int fullKeyIndex = 0;
int partBeginStrIndex = 0;
boolean fullKeyIndexFound = false;
// NOTE: because indexKeyOffsets may be out of order, we must re-loop for each offset
while(!fullKeyIndexFound && partBeginStrIndex < fullKey.length()) {
int partEndStrIndex = fullKey.indexOf(delim, partBeginStrIndex);
if (fullKeyIndex == indexKeyOffset) {
if (indexKey.length() > 0) {
indexKey.append(delim);
}
indexKey.append(fullKey.substring(partBeginStrIndex, partEndStrIndex));
fullKeyIndexFound = true;
} else {
partBeginStrIndex = partEndStrIndex + delim.length();
fullKeyIndex++;
}
}
}
return (indexKey.length() > 0) ? indexKey.toString() : null;
}
/**
* Extracts an index key using an array of key part positions in a main key.
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support.</p>
*/
protected static List<Integer> extractIndexKeyOffsets(String[] splitIndexKeyFormat, String[] splitKeyFormat) {
return extractIndexKeyOffsets(new ArrayList<>(splitKeyFormat.length),
splitIndexKeyFormat, splitKeyFormat);
}
/**
* Extracts an index key using an array of key part positions in a main key.
* <p>SCIPIO: 2.1.0: Added for UtilCache.Index support.</p>
*/
protected static <C extends Collection<V>, V> C extractIndexKeyOffsets(C out, String[] splitIndexKeyFormat, String[] splitKeyFormat) {
if (UtilValidate.isEmpty(splitIndexKeyFormat)) {
throw new IllegalArgumentException("splitIndexKeyFormat empty");
}
if (UtilValidate.isEmpty(splitKeyFormat)) {
throw new IllegalArgumentException("splitKeyFormat empty");
}
for (int i = 0; i < splitIndexKeyFormat.length; i++) {
// If it's a number, use as index; otherwise map to index by name
int k;
try {
k = Integer.parseInt(splitIndexKeyFormat[i]);
out.add(UtilGenerics.cast(k));
continue;
} catch(NumberFormatException e) {
}
boolean found = false;
k = 0;
for(; k < splitKeyFormat.length; k++) {
if (splitIndexKeyFormat[i].equals(splitKeyFormat[k])) {
found = true;
break;
}
}
if (found) {
out.add(UtilGenerics.cast(k));
} else {
Debug.logError("Invalid index key format - invalid key part name [" + splitIndexKeyFormat[i] +
"] from splitIndexKeyFormat " + Arrays.toString(splitIndexKeyFormat) + " splitKeyFormat " +
Arrays.toString(splitKeyFormat) + "; ignoring", module);
}
}
return out;
}
// SCIPIO: 2.1.0: Extended overload.
public static <K, V> UtilCache<K, V> getOrCreateUtilCache(String name, int sizeLimit, int maxInMemory, long expireDelay, boolean useSoftReference, String keyFormat, Map<String, ?> indexDefs, String... propNames) {
// SCIPIO: 2.1.0: Refactored
return getOrCreate(name, () -> new Builder<K, V>()
.sizeLimit(sizeLimit)
.maxInMemory(maxInMemory)
.expireDelay(expireDelay)
.useSoftReference(useSoftReference)
.keyFormat(keyFormat)
.indexDefs(indexDefs)
.propNames(propNames)
);
//String cacheName = name + getNextDefaultIndex(name);
//UtilCache<K, V> cache = new UtilCache<>(cacheName, sizeLimit, maxInMemory, expireDelay, useSoftReference, keyFormat, indexDefs, name, names);
}
public static <K, V> UtilCache<K, V> getOrCreateUtilCache(String name, int sizeLimit, int maxInMemory, long expireDelay, boolean useSoftReference, String... propNames) {
return getOrCreateUtilCache(name, sizeLimit, maxInMemory, expireDelay, useSoftReference, (String) null, (Map<String, ?>) null, propNames);
}
public static <K, V> UtilCache<K, V> createUtilCache(String name, int sizeLimit, int maxInMemory, long expireDelay, boolean useSoftReference, String... propNames) {
// SCIPIO: 2.1.0: Refactored
return create(name, new Builder<K, V>()
.sizeLimit(sizeLimit)
.maxInMemory(maxInMemory)
.expireDelay(expireDelay)
.useSoftReference(useSoftReference)
.propNames(propNames)
);
//String cacheName = name + getNextDefaultIndex(name);
//return storeCache(new UtilCache<>(cacheName, sizeLimit, maxInMemory, expireTime, useSoftReference, null, null, name, names));
}
public static <K, V> UtilCache<K, V> createUtilCache(String name, int sizeLimit, int maxInMemory, long expireDelay, boolean useSoftReference) {
// SCIPIO: 2.1.0: Refactored
return create(name, new Builder<K, V>()
.sizeLimit(sizeLimit)
.maxInMemory(maxInMemory)
.expireDelay(expireDelay)
.useSoftReference(useSoftReference)
);
//String cacheName = name + getNextDefaultIndex(name);
//return storeCache(new UtilCache<>(cacheName, sizeLimit, maxInMemory, expireDelay, useSoftReference, null, null, name));
}
public static <K,V> UtilCache<K, V> createUtilCache(String name, int sizeLimit, long expireDelay, boolean useSoftReference) {
// SCIPIO: 2.1.0: Refactored
return create(name, new Builder<K, V>()
.sizeLimit(sizeLimit)
.expireDelay(expireDelay)
.useSoftReference(useSoftReference)
);
//String cacheName = name + getNextDefaultIndex(name);
//return storeCache(new UtilCache<>(cacheName, sizeLimit, sizeLimit, expireDelay, useSoftReference, null, null, name));
}
public static <K,V> UtilCache<K, V> createUtilCache(String name, int sizeLimit, long expireDelay) {
// SCIPIO: 2.1.0: Refactored
return create(name, new Builder<K, V>()
.sizeLimit(sizeLimit)
.expireDelay(expireDelay)
);
//String cacheName = name + getNextDefaultIndex(name);
//return storeCache(new UtilCache<>(cacheName, sizeLimit, sizeLimit, expireDelay, false, null, null, name));
}
public static <K,V> UtilCache<K, V> createUtilCache(int sizeLimit, long expireDelay) {
// SCIPIO: 2.1.0: Refactored
return create("specified", new Builder<K, V>()
.sizeLimit(sizeLimit)
.expireDelay(expireDelay)
);
//String cacheName = "specified" + getNextDefaultIndex("specified");
//return storeCache(new UtilCache<>(cacheName, sizeLimit, sizeLimit, expireDelay, false, null, null, "specified"));
}
/**
* Creates a util cache with the given simple name.
* <p>SCIPIO: NOTE: Due to legacy behavior, this method falls backs on <code>default.*=</code> settings
* in cache.properties when specific cache settings are not present.</p>
* <p>SCIPIO: 2.1.0: This method was fixed to no longer accidentally prioritize <code>default.*=</code> properties over
* specific cache name properties.</p>
*/
public static <K,V> UtilCache<K, V> createUtilCache(String name, boolean useSoftReference) {
// SCIPIO: 2.1.0: Refactored
return create(name, new Builder<K, V>()
.useSoftReference(useSoftReference)
.propNames("default", name)
);
//String cacheName = name + getNextDefaultIndex(name);
//return storeCache(new UtilCache<>(cacheName, 0, 0, 0, useSoftReference, null, null, "default", name));
}
/**
* Creates a util cache with the given simple name.
* <p>SCIPIO: NOTE: Due to legacy behavior, this method falls backs on <code>default.*=</code> settings
* in cache.properties when specific cache settings are not present.</p>
* <p>SCIPIO: 2.1.0: This method was fixed to no longer accidentally prioritize <code>default.*=</code> properties over
* specific cache name properties.</p>
*/
public static <K,V> UtilCache<K, V> createUtilCache(String name) {
// SCIPIO: 2.1.0: Refactored
return create(name, new Builder<K, V>()
.propNames("default", name)
);
//String cacheName = name + getNextDefaultIndex(name);
//return storeCache(new UtilCache<>(cacheName, 0, 0, 0, false, null, null, "default", name));
}
public static <K,V> UtilCache<K, V> createUtilCache() {
// SCIPIO: 2.1.0: Refactored
return create(null, new Builder<K, V>());
//String cacheName = "default" + getNextDefaultIndex("default");
//return storeCache(new UtilCache<>(cacheName, 0, 0, 0, false, null, null, "default"));
}
/**
* SCIPIO: Creates UtilCache without storing. FOR TESTING ONLY.
* @deprecated SCIPIO: 2.1.0: Use {@link Builder} instead.
* <p>
* Added 2018-09-14.
*/
@Deprecated
public static <K, V> UtilCache<K, V> createOnlyUtilCache(String name, int sizeLimit, int maxInMemory, long expireDelay, boolean useSoftReference, String... propNames) {
// SCIPIO: 2.1.0: Refactored
return new Builder<K, V>(propNames.length > 0 ? propNames[propNames.length - 1] : name)
.name(name)
.sizeLimit(sizeLimit)
.maxInMemory(maxInMemory)
.expireDelay(expireDelay)
.useSoftReference(useSoftReference)
.propNames(propNames)
.build();
//return new UtilCache<>(cacheName, sizeLimit, maxInMemory, expireDelay, useSoftReference, null, null, propName, propNames);
}
@SuppressWarnings("unchecked")
public static <K, V> UtilCache<K, V> findCache(String cacheName) {
return (UtilCache<K, V>) UtilCache.utilCacheTable.get(cacheName);
}
@Override
public void onEviction(Object key, CacheLine<V> cacheLine) {
if (!indexList.isEmpty()) { // SCIPIO: 2.1.0: Added for UtilCache.Index support
removeCacheLineFromIndexes(cacheLine);
}
// SCIPIO: 2.1.0: Added cancel for consistency.
// TODO: REVIEW: It's unclear this is needed but may help guarantee soft refs clear
cancel(cacheLine);
// SCIPIO: removePulse becomes expensive and is not strictly needed because deletions are by CacheLine not by key and putIfAbsent() helps
//ExecutionPool.removePulse(cacheLine);
}
/**
* @deprecated SCIPIO: use overload without useFileSystemStore (flag ignored - 2018-08-20).
*/
@Deprecated
public static <K, V> UtilCache<K, V> getOrCreateUtilCache(String name, int sizeLimit, int maxInMemory, long expireTime, boolean useSoftReference, boolean useFileSystemStore, String... names) {
Debug.logWarning("Deprecated method called: getOrCreateUtilCache with useFileSystemStore", module);
return getOrCreateUtilCache(name, sizeLimit, maxInMemory, expireTime, useSoftReference, names);
}
/**
* @deprecated SCIPIO: use overload without useFileSystemStore (flag ignored - 2018-08-20).
*/
@Deprecated
public static <K, V> UtilCache<K, V> createUtilCache(String name, int sizeLimit, int maxInMemory, long expireTime, boolean useSoftReference, boolean useFileSystemStore, String... propNames) {
Debug.logWarning("Deprecated method called: createUtilCache with useFileSystemStore", module);
return createUtilCache(name, sizeLimit, maxInMemory, expireTime, useSoftReference, propNames);
}
/**
* @deprecated SCIPIO: use overload without useFileSystemStore (flag ignored - 2018-08-20).
*/
@Deprecated
public static <K, V> UtilCache<K, V> createUtilCache(String name, int sizeLimit, int maxInMemory, long expireTime, boolean useSoftReference, boolean useFileSystemStore) {
Debug.logWarning("Deprecated method called: createUtilCache with useFileSystemStore", module);
return createUtilCache(name, sizeLimit, maxInMemory, expireTime, useSoftReference);
}
/**
* @deprecated SCIPIO: no longer implemented (always returns false - 2018-08-20).
*/
@Deprecated
public boolean getUseFileSystemStore() {
Debug.logWarning("Deprecated method called: getUseFileSystemStore", module);
return false;
}
}