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();
         }
      }
   }
}