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