CacheManager.java
/*
** Module : CacheManager.java
** Abstract : Manages caches in the persistent package based on the directory configuration.
**
** Copyright (c) 2023-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- -----------------------------------Description-----------------------------------
** 001 DDF 20230613 Created initial version.
** DDF 20230627 Improved logging, removed initialized member, added getCacheSize and made sure
** that caches are initialized even if binding fails.
** DDF 20230706 Added an additional createLRUCache wrapper method which will use a null
** discriminator by default.
** 002 DDF 20240216 Initialize DynamicQueryHelper and DynamicValidationHelper caches through
** CacheManager.
** 003 CA 20240924 Added an API to create a LinkedHashMap cache, for cases where the cache needs to be
** iterated.
** 004 SP 20241016 Initialize AbstractQuery caches through CacheManager.
** 005 ICP 20250211 Added APIs for creating an Array and an IdentityHashMap caches.
** Initialized the integer, int64 and character caches.
** Changed the default directory.xml location from persistence/cache-size to /cache-size.
** The old location is still processed and cache sizes are instantiated, but a deprecation
** warning will be logged for caches defined there. Cache sizes specified in the new
** location take priority over those in the deprecated path.
** 006 AL2 20250530 Allow discriminator to avoid looking up for class level cache sizes if prefixed with "!".
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist;
import java.util.Map;
import java.util.function.Function;
import java.util.HashMap;
import java.util.*;
import com.goldencode.cache.*;
import com.goldencode.p2j.directory.DirectoryService;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.CentralLogger;
/**
* Retrieves cache sizes from the directory configuration at initialization. Classes that
* have their cache size configured can create a LRUCache/HashMap with the configured capacity.
* If there is no cache size specified for a class, the default value provided is used instead.
*/
public class CacheManager
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(CacheManager.class.getName());
/** Map that contains cache values for each class (even multiple cache values) */
public static Map<ClassKey, Integer> cacheValues;
/**
* Initializes the cacheValues map and if the configuration from the directory
* file can be accessed, it will populate the map with the configuration values
* found for each class. Note that a class can have multiple caches and can be
* distinguished by a discriminatory value used to create a ClassKey instance.
* If only one cache is available for a class, the discriminator can be {@code null}.
*/
public static void initialize()
{
DirectoryService ds = DirectoryService.getInstance();
try
{
if (ds != null)
{
cacheValues = new HashMap<>();
if (!ds.bind())
{
throw new RuntimeException("Directory bind failed");
}
try
{
// First, read from the deprecated location
Utils.enumDirectoryNodes(ds,
"persistence/cache-size",
false, (nodePath) -> processCacheNode(ds,
"persistence/cache-size",
nodePath));
// Then, read from the new location, which will override previous values
Utils.enumDirectoryNodes(ds,
"cache-size",
false,
(nodePath) -> processCacheNode(ds, "cache-size", nodePath));
}
finally
{
ds.unbind();
}
}
}
finally
{
// The following part initializes all the caches that are handled by the configuration.
// Even if the configuration misses any of the cache sizes for the classes, a
// default value can be used which is specified in the method that creates the cache.
// There are also caches that are not static and are initialized in their specific class.
FQLPreprocessor.initializeCache();
AbstractQuery.initializeCache();
SortCriterion.initializeCache();
DynamicQueryHelper.initializeCache();
DynamicValidationHelper.initializeCache();
integer.initializeCache();
int64.initializeCache();
character.initializeCache();
}
}
/**
* Processes a cache node and retrieves its cache size from the directory service.
*
* @param ds
* The directory service.
* @param cacheSizeContainerName
* The base path of the cache configuration (e.g., "cache-size").
* @param nodePath
* The specific node name under this path.
* @return {@code true} to continue processing, {@code false} if an error occurs.
*/
private static boolean processCacheNode(DirectoryService ds,
String cacheSizeContainerName,
String nodePath)
{
nodePath = cacheSizeContainerName + "/" + nodePath;
String className = Utils.getDirectoryNodeString(ds, nodePath + "/class-name", null, false);
int cacheSize = Utils.getDirectoryNodeInt(ds, nodePath + "/size", -1, false);
String discriminator = Utils.getDirectoryNodeString(ds,
nodePath + "/discriminator",
null,
false);
if (className == null)
{
LOG.severe("Configuration class at " + nodePath + " was not specified.");
return false;
}
if (cacheSize < 0)
{
LOG.severe("Missing configuration size for " + className + " or negative size was given.");
return false;
}
try
{
Class<?> clazz = Class.forName(className);
ClassKey key = new ClassKey(clazz, discriminator);
// Check if the entry is from the deprecated location
if (cacheSizeContainerName.equals("persistence/cache-size"))
{
LOG.warning("The cache size for " + className + " is initialized from a deprecated location. " +
"Please move it from /server/{default|<serverID>}/persistent/cache-size to " +
"/server/{default|<serverID>}/cache-size.");
}
cacheValues.put(key, cacheSize);
}
catch (ClassNotFoundException e)
{
if (discriminator != null)
{
LOG.severe(discriminator +
" cache size could not be retrieved for " +
className +
". Specified class does not exist.");
}
else
{
LOG.severe("Cache size could not be retrieved for " +
className +
". Specified class does not exist.");
}
}
return true;
}
/**
* Creates a LRUCache instance based on the parameters specified.
*
* This method is a wrapper for <code>createLRUCache(Class, String, int)</code>,
* it invokes the method assuming the last parameter is {@code null}.
*
* @param <K>
* The key type.
* @param <V>
* The value type.
* @param clazz
* The class for which the cache is created.
* @param defaultSize
* The default size of the cache if it doesn't exist in the configuration.
*
* @return A LRUCache instance.
*/
public static<K, V> LRUCache<K, V> createLRUCache(Class<?> clazz, int defaultSize)
{
return createLRUCache(clazz, null, defaultSize);
}
/**
* Creates a LRUCache instance based on the parameters specified.
*
* This method is a wrapper for <code>createLRUCache(Class, String, int, Function)</code>,
* it invokes the method assuming the last parameter is {@code null}.
*
* @param <K>
* The key type.
* @param <V>
* The value type.
* @param clazz
* The class for which the cache is created.
* @param discriminator
* The value used to differentiate between caches of the same class.
* @param defaultSize
* The default size of the cache if it doesn't exist in the configuration.
*
* @return A LRUCache instance.
*/
public static<K, V> LRUCache<K, V> createLRUCache(Class<?> clazz,
String discriminator,
int defaultSize)
{
return createLRUCache(clazz, discriminator, defaultSize, null);
}
/**
* Creates a LRUCache instance based on the parameters specified.
*
* This method is a wrapper for <code>createLRUCache(Class, String, int, Function, CapacityPolicy)</code>,
* it invokes the method assuming the last parameter is {@code null}.
*
* @param <K>
* The key type.
* @param <V>
* The value type.
* @param clazz
* The class for which the cache is created.
* @param discriminator
* The value used to differentiate between caches of the same class.
* @param defaultSize
* The default size of the cache if it doesn't exist in the configuration.
* @param expirationTest
* A function which confirms whether an expiration candidate element may actually be expired.
*
* @return A LRUCache instance.
*/
public static<K, V> LRUCache<K, V> createLRUCache(Class<?> clazz,
String discriminator,
int defaultSize,
Function<V, Boolean> expirationTest)
{
return createLRUCache(clazz, discriminator, defaultSize, expirationTest, null);
}
/**
* Creates a LRUCache instance based on the parameters specified.
*
* @param <K>
* The key type.
* @param <V>
* The value type.
* @param clazz
* The class for which the cache is created.
* @param discriminator
* The value used to differentiate between caches of the same class.
* @param defaultSize
* The default size of the cache if it doesn't exist in the configuration.
* @param expirationTest
* A function which confirms whether an expiration candidate element may actually be expired.
* @param capacityPolicy
* The policy to follow when capacity cannot be ensured, due to expirationTest vetoing
* expiration candidates.
*
* @return A LRUCache instance based on the parameters specified that has a default/configured capacity.
*/
public static<K, V> LRUCache<K, V> createLRUCache(Class<?> clazz,
String discriminator,
int defaultSize,
Function<V, Boolean> expirationTest,
CapacityPolicy capacityPolicy)
{
Integer cacheSize = getCacheSize(clazz, discriminator, defaultSize);
String name = clazz.toString();
if (discriminator != null)
{
name = name + " " + discriminator;
}
return new LRUCache<K, V>(name, cacheSize, expirationTest, capacityPolicy);
}
/**
* Create a map initialized with the default or configured initial size.
*
* @param <K>
* The key type.
* @param <V>
* The value type.
* @param clazz
* The class for which the cache is created.
* @param discriminator
* The value used to differentiate between caches of the same class.
* @param defaultSize
* The default size of the cache if it doesn't exist in the configuration.
*
* @return A HashMap with a default or a configured capacity.
*/
public static<K, V> Map<K, V> createMapCache(Class<?> clazz, String discriminator, int defaultSize)
{
Integer cacheSize = getCacheSize(clazz, discriminator, defaultSize);
return new HashMap<K, V>(cacheSize);
}
/**
* Create a map initialized with the default or configured initial size.
* <p>
* This linked map version of the API ensures faster iteration of the map via {@link Map#forEach}.
*
* @param <K>
* The key type.
* @param <V>
* The value type.
* @param clazz
* The class for which the cache is created.
* @param discriminator
* The value used to differentiate between caches of the same class.
* @param defaultSize
* The default size of the cache if it doesn't exist in the configuration.
*
* @return A HashMap with a default or a configured capacity.
*/
public static<K, V> Map<K, V> createLinkedMapCache(Class<?> clazz, String discriminator, int defaultSize)
{
Integer cacheSize = getCacheSize(clazz, discriminator, defaultSize);
return new LinkedHashMap<K, V>(cacheSize);
}
/**
* Creates an IdentityHashMap-based cache with the default or configured size.
*
* @param <K>
* The key type of the cache.
* @param <V>
* The value type of the cache.
* @param clazz
* The class for which the cache is created.
* @param discriminator
* The value used to differentiate between caches of the same class.
* @param defaultSize
* The default size of the cache if it doesn't exist in the configuration.
*
* @return An IdentityHashMap with an initial capacity based on the configured or default size.
*/
public static <K, V> Map<K, V> createIdentityHashMapCache(Class<?> clazz,
String discriminator,
int defaultSize)
{
int cacheSize = getCacheSize(clazz, discriminator, defaultSize);
return new IdentityHashMap<>(cacheSize);
}
/**
* Creates an array-based cache initialized with the default or configured size.
*
* @param <T>
* The type of elements stored in the array.
* @param clazz
* The class for which the cache is created.
* @param discriminator
* The value used to differentiate between caches of the same class.
* @param defaultSize
* The default size of the cache if it doesn't exist in the configuration.
* @param type
* The class type of the array elements.
*
* @return A generic array with the configured size.
*/
public static <T> T[] createArrayCache(Class<?> clazz,
String discriminator,
int defaultSize,
Class<T> type)
{
int cacheSize = getCacheSize(clazz, discriminator, defaultSize);
return (T[]) java.lang.reflect.Array.newInstance(type, cacheSize);
}
/**
* Method that looks for the configured size of a cache and returns it's size.
* If a discriminator is provided but the ClassKey made of the given clazz
* and discriminator can't be found, the {@code null} will also be used
* as a discriminator, but only if it was not originally {@code null}.
*
* @param clazz
* The class for which the cache is created.
* @param discriminator
* The value used to differentiate between caches of the same class.
* @param defaultSize
* The default size of the cache if it doesn't exist in the configuration.
*
* @return The size of the cache if it's configured, or the default size.
*/
public static int getCacheSize(Class<?> clazz, String discriminator, int defaultSize)
{
boolean enforceDiscriminator = false;
if (discriminator != null && discriminator.startsWith("!"))
{
enforceDiscriminator = true;
discriminator = discriminator.substring(1);
}
ClassKey key = new ClassKey(clazz, discriminator);
if (cacheValues != null)
{
Integer size = cacheValues.get(key);
if (size != null)
{
return size;
}
else if (discriminator != null && !enforceDiscriminator)
{
ClassKey defaultKey = new ClassKey(clazz, null);
size = cacheValues.get(defaultKey);
if (size != null)
{
return size;
}
}
}
return defaultSize;
}
/**
* Represents the key used to differentiate between multiple classes
* when retrieving their cache sizes.
*/
public static class ClassKey
{
/** The class that has a cache size configured */
private Class<?> clazz;
/** The discriminator used to differentiate between multiple cache size of the same class */
private String discriminator;
/**
* Constructor.
*
* @param clazz
* The class.
* @param discriminator
* The discriminator.
*/
public ClassKey(Class<?> clazz, String discriminator)
{
this.clazz = clazz;
this.discriminator = discriminator;
}
/**
* Getter for the clazz property.
*
* @return The clazz property.
*/
public Class<?> getClazz()
{
return clazz;
}
/**
* Getter for the discriminator property.
*
* @return The discriminator property.
*/
public String getDiscriminator()
{
return discriminator;
}
/**
* Calculate a hash code for this object.
*
* @return Hash code.
*/
@Override
public int hashCode()
{
int hash = 37 * clazz.hashCode();
if (discriminator != null)
{
hash += discriminator.hashCode();
}
return hash;
}
/**
* Test this object for equality (equivalence) with the given object.
*
* @param o
* Object to test.
*
* @return <code>true</code> if the object are equal, <code>false</code> otherwise.
*/
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o == null)
{
return false;
}
if (!(o instanceof ClassKey))
{
return false;
}
ClassKey classKey = (ClassKey) o;
if (discriminator != null)
{
return this.clazz == classKey.getClazz() &&
this.discriminator.equals(classKey.getDiscriminator());
}
else
{
return this.clazz == classKey.getClazz();
}
}
}
}