ConversionPool.java

/*
** Module   : ConversionPool.java
** Abstract : Shared pool of pattern engines for runtime conversion use.
**
** Copyright (c) 2014-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20140405 Created initial version as a performance enhancement.
** 002 ECF 20140422 Added NPE safety in runTask.
** 003 ECF 20150331 Added variant of runTask to allow passing results from one task to the next
**                  in a pipeline.
** 004 ECF 20150609 Made pool variable final.
** 005 ECF 20150910 Do not initialize standard schema p2o data if the current application does
**                  not use metadata.
** 006 ECF 20160907 Improved AST string interning for performance and added isInitialized method.
** 007 CA  20180520 Initialize all databases for runtime support.
** 008 OM  20220214 Better descriptive messages and lowered error level for dynamic schema loading logs.
**     OM  20220330 Moved conversion artifacts to ${cvtpath} folder.
**     OM  20220412 The .p20 resources are loaded using the same API like .dict.
** 009 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 010 CA  20230722 Initialize the default-databases at conversion startup (both as a set and individual 
**                  databases).
*/

/*
** 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.*;
import java.util.logging.*;
import com.goldencode.ast.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.pattern.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.Utils.DirScope;
import com.goldencode.p2j.util.logging.*;

/**
 * A shared pool of {@link com.goldencode.p2j.pattern.PatternEngine pattern engines} used for
 * runtime conversion of dynamically defined persistence constructs, such as temp-tables and
 * queries. A shared pool is used, rather than instantiating pattern engines as needed, because
 * these objects are very expensive to create and configure. The instances in the pool are
 * created and configured to their specific conversion task once, then stored for use many times
 * across contexts.
 * <p>
 * A conversion task is performed by invoking the {@link #runTask(ConversionProfile, Aast...)}
 * method, which requires the caller to specify a profile which defines the conversion task to
 * be performed, and to provide one or more target ASTs on which to operate. Any objects stored
 * during the conversion task are available through the {@link Results} object returned.
 */
public final class ConversionPool
{
   /** Logger */
   private static CentralLogger LOG = CentralLogger.get(ConversionPool.class);
   
   /** Singleton instance of this class */
   private static ConversionPool instance = null;
   
   /** Map of pattern engine stacks, keyed by conversion profile */
   private final Map<ConversionProfile, Stack<PatternEngine>> pool = new HashMap<>();
   
   /**
    * Private constructor.
    */
   private ConversionPool()
   {
   }
   
   /**
    * Initialize the conversion pool by loading all necessary, permanent schemas and creating the
    * initial pattern engine instances.
    * 
    * @throws  ConfigurationException
    *          if there is an error loading schemas or creating pattern engine instances.
    * @throws  IllegalStateException
    *          if this method is called more than once.
    */
   public synchronized static void initialize()
   throws ConfigurationException
   {
      if (instance != null)
      {
         throw new IllegalStateException("Conversion pool must not be initialized more than once");
      }
      
      long start = System.currentTimeMillis();
      
      // load the permanent schemas needed for conversion
      
      SchemaConfig sc = Configuration.getSchemaConfig();
      Iterator<String> dbIter = sc.databases();
      List<String> all = new ArrayList<>();
      while (dbIter.hasNext())
      {
         all.add(dbIter.next());
      }
      
      String metaName = sc.getMetadata().getName();
      all.add(metaName);
      boolean metaActive = sc.isMetadataActive();
      
      // load the schema dict files into the user's in-memory AST storage
      Map<String, Aast> pristineDbAsts = new HashMap<>();
      XmlFilePlugin fileLoader = new XmlFilePlugin(null, true);
      
      for (String schema : all)
      {
         try
         {
            String schemaPath = sc.getSchemaResource(schema, ".dict");
            if (schemaPath == null)
            {
               throw new ConfigurationException(
                  "The schema '" + schema + "' does not have a .dict file set up in the configuration file.");
            }
            
            // load the schema
            Aast ast = fileLoader.loadTree(schemaPath);
            ast.intern();
            pristineDbAsts.put(schemaPath, ast);
            
            if (!metaActive && schema.equals(metaName))
            {
               continue;
            }
            
            // load the .p2o definitions in a similar fashion to .dict, above
            String p2oPath = sc.getSchemaResource(schema, ".p2o");
            if (p2oPath == null)
            {
               throw new ConfigurationException(
                     "The '" + schema + ".p2o' file containing definitions for '" + schema + 
                     "' schema could not be located by the classloader.");
            }
            
            ast = fileLoader.loadTree(p2oPath);
            ast.intern();
            pristineDbAsts.put(p2oPath, ast);
         }
         catch (AstException | ConfigurationException exc)
         {
            String msg = "Failed to load the pristine '" + schema + "' schema information from file.";
            
            if (LOG.isLoggable(Level.WARNING))
            {
               LOG.log(Level.WARNING, msg, exc);
            }
            
            throw new RuntimeException(msg, exc);
         }
      }
      
      for (String schema : all)
      {
         String schemaPath = sc.getSchemaResource(schema, ".dict");
         
         // load the schema
         if (pristineDbAsts.containsKey(schemaPath))
         {
            Aast schemaAst = pristineDbAsts.get(schemaPath);
            schemaAst.intern();
            AstManager.get().saveTree(schemaAst, schemaPath, false);
         }
         
         // load the .p2o definitions
         String p2oPath = schemaPath.replaceAll("\\.dict", ".p2o");
         
         if (pristineDbAsts.containsKey(p2oPath))
         {
            Aast p2oAst = pristineDbAsts.get(p2oPath);
            p2oAst.intern();
            AstManager.get().saveTree(p2oAst, p2oPath, false);
         }
      }
      
      instance = new ConversionPool();
      
      // add pattern engines
      for (ConversionProfile profile : ConversionProfile.values())
      {
         // TODO: use directory-based configuration to initialize number of engines per profile
         instance.addEngine(profile);
      }
      
      // initialize all the SchemaDictionary for default-databases
      DirectoryService ds = DirectoryService.getInstance();
      if (ds != null && ds.bind())
      {
         try
         {
            String path = Utils.findDirectoryNodePath(ds, "default-databases", null, 
                                                      DirScope.SERVER, null);
            if (path != null)
            {
               String[] pkgs = ds.enumerateNodes(path);
               for (String pkg : pkgs)
               {
                  String[] dbs = ds.getNodeStrings(path + "/" + pkg, "values");
                  
                  SortedSet<String> defDbs = new TreeSet<>();
                  for (int i = 0; i < dbs.length; i++)
                  {
                     defDbs.add(dbs[i]);
                     SchemaDictionary.localInstance(new TreeSet<>(Arrays.asList(dbs[i])));
                  }

                  SchemaDictionary.localInstance(defDbs);
               }
            }
         }
         finally
         {
            ds.unbind();
         }
      }
      
      
      if (LOG.isLoggable(Level.INFO))
      {
         long elapsed = System.currentTimeMillis() - start;
         LOG.log(Level.INFO, "Runtime conversion pool initialized in " + elapsed + " ms.");
      }
   }
   
   /**
    * Indicate whether the conversion pool was initialized successfully.
    * 
    * @return  {@code true} if initialized, else {@code false}.
    */
   public synchronized static boolean isInitialized()
   {
      return instance != null;
   }
   
   /**
    * Run a specific conversion task against the given ASTs, as defined by the provided profile.
    * 
    * @param   profile
    *          Enumeration value defining the conversion profile to apply to the target ASTs.
    * @param   targetAsts
    *          Source ASTs upon which to operate.
    * 
    * @return  An object which contains any objects stored in the pattern engine as a result of
    *          the conversion task.
    */
   static Results runTask(ConversionProfile profile, Aast... targetAsts)
   {
      return runTask(profile, null, targetAsts);
   }
   
   /**
    * Run a specific conversion task against the given ASTs, as defined by the provided profile.
    * 
    * @param   profile
    *          Enumeration value defining the conversion profile to apply to the target ASTs.
    * @param   initialValues
    *          A {@link Results} object from a previous task which is used to initialize the
    *          pattern engine for this task with a set of starting values. This provides a way to
    *          pass results from one task's pattern engine to the next in a pipeline. May be
    *          <code>null</code>.
    * @param   targetAsts
    *          Source ASTs upon which to operate.
    * 
    * @return  An object which contains any objects stored in the pattern engine as a result of
    *          the conversion task.
    */
   static Results runTask(ConversionProfile profile, Results initialValues, Aast... targetAsts)
   {
      PatternEngine engine = null;
      
      try
      {
         // get a pattern engine from the pool
         engine = instance.popEngine(profile);
         
         // add the initial values, if any, to the engine's stored objects.
         if (initialValues != null)
         {
            Map<PatternEngine.StorageKey, Object> storedObjects = initialValues.storedObjects;
            for (Map.Entry<PatternEngine.StorageKey, Object> entry : storedObjects.entrySet())
            {
               engine.storeObject(entry.getKey(), entry.getValue());
            }
         }
         
         // add the target ASTs
         for (Aast ast : targetAsts)
         {
            engine.addTargetAst(ast);
         }
         
         // run the conversion
         engine.run();
         
         // collect and return results
         return (new Results(engine.getAllStoredObjects()));
      }
      finally
      {
         // return the pattern engine to the pool
         if (engine != null)
         {
            engine.clearStoredObjects();
            instance.pushEngine(profile, engine);
         }
      }
   }
   
   /**
    * Add a pattern engine instance to the stack associated with the given conversion profile.
    * 
    * @param   profile
    *          Conversion profile which will determine the task performed by the new pattern
    *          engine instance.
    * @throws  ConfigurationException
    *          if there is an error instantiating and initializing the new pattern engine object.
    */
   private void addEngine(ConversionProfile profile)
   throws ConfigurationException
   {
      Stack<PatternEngine> stack;
      synchronized (pool)
      {
         stack = pool.computeIfAbsent(profile, conversionProfile -> new Stack<>());
      }
      
      synchronized (stack)
      {
         PatternEngine engine = new PatternEngine(profile.getName(), profile.isConditional());
         stack.push(engine);
         stack.notify();
      }
   }
   
   /**
    * Push a pattern engine instance back onto its stack, so it can be used again.
    * 
    * @param   profile
    *          Profile which defines the pattern engine's conversion task.
    * @param   engine
    *          Pattern engine instance to be pushed.
    */
   private void pushEngine(ConversionProfile profile, PatternEngine engine)
   {
      Stack<PatternEngine> stack;
      synchronized (pool)
      {
         stack = pool.get(profile);
      }
      
      synchronized (stack)
      {
         stack.push(engine);
         stack.notify();
      }
   }
   
   /**
    * Pop a pattern engine instance from its stack in order to perform a conversion task.
    * 
    * @param   profile
    *          Profile which defines the pattern engine's conversion task.
    * 
    * @return  Pattern engine instance.
    */
   private PatternEngine popEngine(ConversionProfile profile)
   {
      Stack<PatternEngine> stack;
      synchronized (pool)
      {
         stack = pool.get(profile);
      }
      
      PatternEngine engine;
      synchronized (stack)
      {
         while (stack.isEmpty())
         {
            try
            {
               stack.wait();
            }
            catch (InterruptedException exc)
            {
               // TODO: check whether spurious or real termination
            }
         }
         
         engine = stack.pop();
      }
      
      return engine;
   }
   
   /**
    * Object which holds the objects stored in the pattern engine by its conversion task.
    */
   static class Results
   {
      /** Map of stored objects */
      private final Map<PatternEngine.StorageKey, Object> storedObjects;
      
      /**
       * Constructor.
       * 
       * @param   storedObjects
       *          Map of stored objects.
       */
      private Results(Map<PatternEngine.StorageKey, Object> storedObjects)
      {
         this.storedObjects = new HashMap<>(storedObjects);
      }
      
      /**
       * Get the object stored under the given id.
       * 
       * @param   id
       *          Object id.
       * 
       * @return  Stored object or <code>null</code> if none is found.
       * 
       * @throws  IllegalStateException
       *          if method is invoked before task has run.
       */
      Object getStoredObject(String id)
      {
         return getStoredObject(null, id);
      }
      
      /**
       * Get the object stored under the given category and id.
       * 
       * @param   category
       *          Object category.
       * @param   id
       *          Object id.
       * 
       * @return  Stored object or <code>null</code> if none is found.
       * 
       * @throws  IllegalStateException
       *          if method is invoked before task has run.
       */
      Object getStoredObject(String category, String id)
      {
         if (storedObjects == null)
         {
            throw new IllegalStateException("Cannot retrieve stored object until task has run");
         }
         
         return storedObjects.get(new PatternEngine.StorageKey(category, id));
      }
   }
}