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