TemplateWorker.java
/*
** Module : TemplateWorker.java
** Abstract : provides functions for accessing J2SE system properties
**
** Copyright (c) 2005-2022, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------------Description----------------------------------
** 001 GES 20050620 @21527 Created initial version supporting a simple
** AST template loading and grafting mechanism.
** 002 GES 20050622 @21541 Added depot services to store and retrieve
** ASTs between rulesets that are disconnected
** in time and place (still requires such
** sharing to occur within a single pattern
** engine run). Also added resolver
** registration to the grafting process.
** 003 ECF 20050627 @21555 Modification to 'graft' user function to be
** more lenient. The source parameter may now be
** null, even if the grafted template contains
** an AST which has a peernode annotation. The
** peernode will still be returned from the
** graft method in this case.
** 004 ECF 20050711 @21688 Added a variant of graft method. This version
** takes an Aast as the parent, rather than an
** ID. This is needed when grafting to a parent
** which is not part of the AST registry.
** 005 GES 20050819 @22186 Added graftAt() support for adding at a
** specific index.
** 006 GES 20051013 @23059 Removed requirement that parent be non-null
** in graft/graftAt variants.
** 007 GES 20090424 @41935 Import change.
** 008 GES 20090429 @42049 Match package and class name changes.
** 009 GES 20090515 @42214 Moved to AstManager from AstRegistry/AstPersister.
** 010 GES 20090518 @42389 Import change.
** 011 SVL 20130624 Allow to load templates at server run time.
** 012 ECF 20140808 Set all AST nodes IDs to zero when loaded from XML, as a safety precaution.
** 013 ECF 20150715 Replace StringBuffer with StringBuilder.
** 014 CA 20170825 Allow graftAt to work without having to attach to a parent.
** 015 CA 20191008 Template ASTs are allowed to have inlined shadow nodes.
** 016 CA 20211214 The AST manager plugin must be context-local, for runtime conversion, as the
** InMemoryRegistryPlugin is not thread-safe.
** All worker's state must be context-local, for runtime conversion to work in
** multi-context mode, as the pattern workers are singletons.
** OM 20220512 Developer mode: allows to load resources from file system if jar is not built.
*/
/*
** 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.pattern;
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import org.w3c.dom.*;
import com.goldencode.ast.*;
import com.goldencode.util.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.security.*;
/**
* Provides a set of user-defined functions for accessing libraries of
* pre-defined AST subtrees. Each AST subtree is called a 'template' and
* can be accessed by a name which must be unique within a given template
* library. Each template library is an XML file stored in a directory
* in the <code>patpath</code>. All template library filenames must end
* in a suffix specified by {@link #TPL_SUFFIX}.
* <p>
* Each template is defined in the format required to be loaded using the
* <code>AstPersister</code>. Each valid template must be rooted at
* an <code>AstPersister.ELEM_ROOT</code> element.
* <p>
* A generic facility for text replacements is provided. This facility is
* implemented after the reading of the elements from the XML file but
* before those elements are converted into an AST subtree by the persister.
* This allows any attribute in the same template to be customized on the fly
* for different purposes. Since the persister translates everything from
* its text form into a binary AST form, the result of these text
* transformations is the same as if the original XML file had been hard
* coded to the runtime values provided. Thus token types, token text and
* other attributes (even annotations) can be transformed in this manner.
* Any replaceable text must be formatted with <code>${key}</code> where
* a list of string-based key/value pairs are passed at runtime and are used
* as the replacements for each matching key found. Note that the
* <code>${</code> and <code>}</code> are only stored in the XML file, they
* are not actually passed as part of the key in the replacements list.
* <p>
* This class also provides depot services to store and retrieve ASTs between
* rulesets that are disconnected in time and place (it still requires such
* sharing to occur within a single pattern engine run). For example, if
* a tree transformation requires a separation between the location at which
* an AST subtree is created and the location at which that subtree is
* attached, the subtree can be stored in the depot under a name and then
* retrieved 'downstream' in the same pattern engine run (the same pipeline).
* Please note that only 1 AST can be stored per name so this facility
* operates like a shared local variable pool of ASTs.
*/
public class TemplateWorker
extends AbstractPatternWorker
{
/** Suffix for all template library filenames. */
public static final String TPL_SUFFIX = ".tpl";
/** Annotation name to mark a template node as the peer node. */
public static final String PEERNODE = "peernode";
/** Context local work area. */
private static final ContextLocal<WorkArea> context = new ContextLocal<WorkArea>()
{
protected WorkArea initialValue()
{
return new WorkArea();
}
};
/**
* Default constructor which calls the super constructor and registers
* libraries with the pattern engine.
*/
public TemplateWorker()
{
super();
setLibrary(new Template());
}
/**
* This is a pattern engine library that provides easy access to
* libraries of pre-defined AST subtrees.
*/
public class Template
{
/** Names a given template. */
private static final String ATTR_NAME = "name";
/** Caches the list of loaded AST templates. */
private final Map<String, Node> templates = new ConcurrentHashMap<>();
/** Caches the list of loaded XML libraries (to avoid reloading). */
private final Set<String> libraries = Collections.newSetFromMap(new ConcurrentHashMap<>());
/**
* Load a named template library from an XML file. The file is named
* <code><name>.<tpl></code>. it must be located somewhere
* in the path defined by the global configuration parameter
* <code>patpath</code>.
*
* @param name
* Root name of the XML library containing AST templates.
*
* @throws ConfigurationException
* if any error occurs loading the template library.
*/
public void load(String name)
throws ConfigurationException
{
// avoid reloading
if (libraries.contains(name))
{
return;
}
Element root = null;
Document dom = null;
try
{
if (Configuration.isRuntimeConfig())
{
InputStream stream = ConfigLoader.getConversionResource(name, TPL_SUFFIX);
if (stream != null)
{
dom = XmlHelper.parse(stream);
}
}
// developer mode: allow resource to be loaded at runtime from file system if present, avoiding
// having the jar built
if (dom == null)
{
File file = ConfigLoader.findConfigurationFile(name, TPL_SUFFIX);
dom = XmlHelper.parse(file);
}
if (dom == null)
{
throw new ConfigurationException("Cannot find '" + name + TPL_SUFFIX + "' resource");
}
root = dom.getDocumentElement();
}
catch (Exception exc)
{
throw new ConfigurationException("Cannot load template library named " + name + ".", exc);
}
// cache our library name so it doesn't get reloaded
libraries.add(name);
// tag name of the element we are looking for
String search = XmlFilePlugin.ELEM_ROOT;
// the list of children will include #Text and other DOM defined
// trash... get the first of our children
Node tpl = root.getFirstChild();
// search for all children that have a tagName of ELEM_ROOT
while (tpl != null)
{
// is this a template?
if (tpl.getNodeName().equals(search))
{
// walk child ast nodes, setting any non-zero id attribute to zero as a safety
// precaution
zeroAstIds((Element) tpl);
// we have a template, get its name and cache it
String tname = ((Element) tpl).getAttribute(ATTR_NAME);
if (tname.trim().length() == 0)
{
String errmsg = "Missing name for element " + search + " in template library " + name + ".";
throw new ConfigurationException(errmsg);
}
templates.put(tname, tpl);
}
tpl = tpl.getNextSibling();
}
}
/**
* Lookup a named template, process any attribute text replacements
* (if specified), attach the new AST subtree as the last child of the
* specified parent and, if any of the AST nodes has a
* <code>PEERNODE</code> attribute AND the <code>source</code>
* parameter is not <code>null</code>, cross-reference that node with
* <code>source</code>, using the respective 'peerid' values. Any
* specified replacement key found in the form <code>${key}</code>
* will be replaced with the matching value. The object array must
* be made as sequential pairs of key string and matching value string.
*
* @param name
* The name of the template to use. This template must have
* been previously loaded using {@link #load}.
* @param source
* The source node to annotate if the <code>PEERNODE</code>
* annotation exists in the AST subtree. May be
* <code>null</code>.
* @param parentid
* The ID of the target AST to be used as a parent for this
* subtree. If <code>null</code> no attachment occurs but the
* tree is still created.
* @param replacements
* An array of key/value string pairs where any keys found
* in the template node's attribute values will be replaced
* with the associated values.
*
* @return The node which was specified as the peer node or the
* root node of the newly created subtree if no peer node
* was specified.
*
* @throws AstException
* if there is any problem creating or attaching the template.
*/
public Aast graft(String name, Aast source, long parentid, Object[] replacements)
throws AstException
{
return graftAt(name, source, parentid, -1, replacements);
}
/**
* Lookup a named template, process any attribute text replacements
* (if specified), attach the new AST subtree as a child of the
* specified parent AT THE SPECIFIED INDEX and, if any of the AST nodes
* has a <code>PEERNODE</code> attribute AND the <code>source</code>
* parameter is not <code>null</code>, cross-reference that node with
* <code>source</code>, using the respective 'peerid' values. Any
* specified replacement key found in the form <code>${key}</code>
* will be replaced with the matching value. The object array must
* be made as sequential pairs of key string and matching value string.
*
* @param name
* The name of the template to use. This template must have
* been previously loaded using {@link #load}.
* @param source
* The source node to annotate if the <code>PEERNODE</code>
* annotation exists in the AST subtree. May be
* <code>null</code>.
* @param parentid
* The ID of the target AST to be used as a parent for this
* subtree. If <code>null</code> no attachment occurs but the
* tree is still created.
* @param index
* 0 to insert as the first child, -1 to insert as the last
* child or a 0-based index to specify an exact child index
* at which to insert the new child. All current children at
* that index or at a greater index, are moved right. If the
* index is greater than the current number of children, the
* new child will be added at the end.
* @param replacements
* An array of key/value string pairs where any keys found
* in the template node's attribute values will be replaced
* with the associated values.
*
* @return The node which was specified as the peer node or the
* root node of the newly created subtree if no peer node
* was specified.
*
* @throws AstException
* if there is any problem creating or attaching the template.
*/
public Aast graftAt(String name, Aast source, long parentid, int index, Object[] replacements)
throws AstException
{
// obtain the parent node
Aast parent = getResolver().getAst(parentid);
if (parent == null)
{
throw new IllegalArgumentException("Invalid parentid: " + parentid);
}
return graftAt(false, name, source, parent, index, replacements);
}
/**
* Lookup a named template, process any attribute text replacements
* (if specified), attach the new AST subtree as the last child of the
* specified parent and, if any of the AST nodes has a
* <code>PEERNODE</code> attribute AND the <code>source</code>
* parameter is not <code>null</code>, cross-reference that node with
* <code>source</code>, using the respective 'peerid' values. Any
* specified replacement key found in the form <code>${key}</code>
* will be replaced with the matching value. The object array must
* be made as sequential pairs of key string and matching value string.
*
* @param name
* The name of the template to use. This template must have
* been previously loaded using {@link #load}.
* @param source
* The source node to annotate if the <code>PEERNODE</code>
* annotation exists in the AST subtree. May be
* <code>null</code>.
* @param parent
* The target AST to be used as a parent for this subtree.
* If <code>null</code> no attachment occurs but the tree
* is still created.
* @param replacements
* An array of key/value string pairs where any keys found
* in the template node's attribute values will be replaced
* with the associated values.
*
* @return The node which was specified as the peer node or the
* root node of the newly created subtree if no peer node
* was specified.
*
* @throws AstException
* if there is any problem creating or attaching the template.
*/
public Aast graft(String name, Aast source, Aast parent, Object[] replacements)
throws AstException
{
return graftAt(false, name, source, parent, -1, replacements);
}
/**
* Lookup a named template, process any attribute text replacements
* (if specified), attach the new AST subtree as a child of the
* specified parent AT THE SPECIFIED INDEX and, if any of the AST nodes
* has a <code>PEERNODE</code> attribute AND the <code>source</code>
* parameter is not <code>null</code>, cross-reference that node with
* <code>source</code>, using the respective 'peerid' values. Any
* specified replacement key found in the form <code>${key}</code>
* will be replaced with the matching value. The object array must
* be made as sequential pairs of key string and matching value string.
*
* @param name
* The name of the template to use. This template must have
* been previously loaded using {@link #load}.
* @param source
* The source node to annotate if the <code>PEERNODE</code>
* annotation exists in the AST subtree. May be
* <code>null</code>.
* @param parent
* The target AST to be used as a parent for this subtree.
* If <code>null</code> no attachment occurs but the tree
* is still created.
* @param index
* 0 to insert as the first child, -1 to insert as the last
* child or a 0-based index to specify an exact child index
* at which to insert the new child. All current children at
* that index or at a greater index, are moved right. If the
* index is greater than the current number of children, the
* new child will be added at the end.
* @param replacements
* An array of key/value string pairs where any keys found
* in the template node's attribute values will be replaced
* with the associated values.
*
* @return The node which was specified as the peer node or the
* root node of the newly created subtree if no peer node
* was specified.
*
* @throws AstException
* if there is any problem creating or attaching the template.
*/
public Aast graftAt(String name,
Aast source,
Aast parent,
int index,
Object[] replacements)
{
return graftAt(false, name, source, parent, index, replacements);
}
/**
* Lookup a named template, process any attribute text replacements
* (if specified), attach the new AST subtree as a child of the
* specified parent AT THE SPECIFIED INDEX and, if any of the AST nodes
* has a <code>PEERNODE</code> attribute AND the <code>source</code>
* parameter is not <code>null</code>, cross-reference that node with
* <code>source</code>, using the respective 'peerid' values. Any
* specified replacement key found in the form <code>${key}</code>
* will be replaced with the matching value. The object array must
* be made as sequential pairs of key string and matching value string.
*
* @param inlineShadows
* Flag indicating that the template needs to have the shadows inlined.
* @param name
* The name of the template to use. This template must have
* been previously loaded using {@link #load}.
* @param source
* The source node to annotate if the <code>PEERNODE</code>
* annotation exists in the AST subtree. May be
* <code>null</code>.
* @param parent
* The target AST to be used as a parent for this subtree.
* If <code>null</code> no attachment occurs but the tree
* is still created.
* @param index
* 0 to insert as the first child, -1 to insert as the last
* child or a 0-based index to specify an exact child index
* at which to insert the new child. All current children at
* that index or at a greater index, are moved right. If the
* index is greater than the current number of children, the
* new child will be added at the end.
* @param replacements
* An array of key/value string pairs where any keys found
* in the template node's attribute values will be replaced
* with the associated values.
*
* @return The node which was specified as the peer node or the
* root node of the newly created subtree if no peer node
* was specified.
*
* @throws AstException
* if there is any problem creating or attaching the template.
*/
public Aast graftAt(boolean inlineShadows,
String name,
Aast source,
Aast parent,
int index,
Object[] replacements)
throws AstException
{
Node tpl = templates.get(name);
if (tpl == null)
{
throw new IllegalArgumentException("Template named '" + name + "' doesn't exist.");
}
// make a temporary deep copy of this subtree
Node template = tpl.cloneNode(true);
HashMap<String, String> replace = null;
// create a map of all parameters to be replaced
if (replacements != null)
{
replace = new HashMap<>();
for (int i = 0; i < replacements.length; i += 2)
{
replace.put(replacements[i] == null ? null : replacements[i].toString(),
replacements[i + 1] == null ? null : replacements[i + 1].toString());
}
}
// "sweep" all replacement keys with their runtime values
processReplacements(template, replace);
// instantiate the AST subtree
Aast result = XmlFilePlugin.loadTree((Element)template, inlineShadows);
// graft and register the subtree
if (parent != null)
{
attach(parent, result, index);
}
// if one of the nodes was marked as the peer node, implement the
// proper peer relationship with the passed source node
Iterator<Aast> iter = result.iterator();
while (iter.hasNext())
{
Aast child = iter.next();
// detect if this node is marked as the peer
if (child.isAnnotation(PEERNODE))
{
// return this node instead of the root
result = child;
// remove the annotation since it was for our use only
result.removeAnnotation(PEERNODE);
// fixup the peer ids
if (source != null)
{
source.putAnnotation("peerid", result.getId());
result.putAnnotation("peerid", source.getId());
}
}
}
return result;
}
/**
* Grafts the child to the parent as the last childe and registers
* the subtree (rooted at the child) with the resolver. All nodes
* within the child subtree will have their IDs and parents fixed up to
* properly integrate with the new tree.
*
* @param parent
* The node at which to graft the child subtree. If
* <code>null</code> this method immediately returns.
* @param child
* The root of the subtree to be grafted.
*
* @throws AstException
* if there is any problem grafting the child to the parent.
*/
public void attach(Aast parent, Aast child)
throws AstException
{
attach(parent, child, -1);
}
/**
* Grafts the child to the parent at the specified index and registers
* the subtree (rooted at the child) with the resolver. All nodes
* within the child subtree will have their IDs and parents fixed up to
* properly integrate with the new tree.
*
* @param parent
* The node at which to graft the child subtree. If
* <code>null</code> this method immediately returns.
* @param child
* The root of the subtree to be grafted.
* @param index
* 0 to insert as the first child, -1 to insert as the last
* child or a 0-based index to specify an exact child index
* at which to insert the new child. All current children at
* that index or at a greater index, are moved right. If the
* index is greater than the current number of children, the
* new child will be added at the end.
*
* @throws AstException
* if there is any problem grafting the child to the parent.
*/
public void attach(Aast parent, Aast child, int index)
throws AstException
{
if (parent == null)
{
// nothing to do
return;
}
// graft the subtree to the parent
parent.graftAt(child, index);
// register all children with the resolver if parent has an ID
if (parent.getId() != null)
{
Iterator<Aast> iter = child.iterator();
while (iter.hasNext())
{
Aast next = iter.next();
getResolver().registerAst(next);
}
}
}
/**
* Adds or replaces the given AST in the depot using the specified name
* as a key by which the AST can be retrieved later.
*
* @param name
* The name to use as a key for the depot.
* @param ast
* The AST to add to the depot.
*
* @return Always <code>true</code>.
*/
public boolean addToDepot(String name, Aast ast)
{
context.get().depot.put(name, ast);
return true;
}
/**
* Returns the AST found in the depot by the given name, removing it
* first. If no such AST exists, <code>null</code> is returned.
*
* @param name
* The name to use as a key for the depot lookup.
*
* @return The AST found or <code>null</code> if no such AST exists.
*/
public Aast removeFromDepot(String name)
{
return context.get().depot.remove(name);
}
/**
* Returns the AST found in the depot by the given name or
* <code>null</code> if no such AST exists.
*
* @param name
* The name to use as a key for the depot lookup.
*
* @return The AST found or <code>null</code> if no such AST exists.
*/
public Aast getFromDepot(String name)
{
return context.get().depot.get(name);
}
/**
* Detects if an AST exists in the depot under the given name.
*
* @param name
* The name to use as a key for the depot lookup.
*/
public boolean isInDepot(String name)
{
return context.get().depot.containsKey(name);
}
/**
* Recursively sweep all attribute values of this node and its subtree,
* based on a map of key/value pairs. Replacements occur for any
* instances of <code>${key}</code> in any attribute value. Once the
* current node's attributes are processed, all child nodes are visited
* and this method is called recursively for each child.
*
* @param tpl
* The node whose attributes must be processed.
* @param replacements
* A collection of key/value pairs.
*/
private void processReplacements(Node tpl, Map<String, String> replacements)
{
// is there anything to do?
if (replacements == null)
{
return;
}
// read all attributes from this node
NamedNodeMap list = tpl.getAttributes();
// are there any attributes?
if (list != null)
{
// loop through elements and sweep each one
int len = list.getLength();
for (int i = 0; i < len; i++)
{
Node next = list.item(i);
// process the value replacements for all ${key} constructs
String replace = StringHelper.sweep(next.getNodeValue(), replacements);
// save our changes
next.setNodeValue(replace);
}
}
// process all children recursively
Node child = tpl.getFirstChild();
while (child != null)
{
processReplacements(child, replacements);
child = child.getNextSibling();
}
}
/**
* As a safety measure, set all AST IDs to 0, so they will have appropriate, available IDs
* applied when the template is grafted to a parent AST.
*
* @param tpl
* Java template XML element, whose descendant elements represent ASTs.
*/
private void zeroAstIds(Element tpl)
{
NodeList descendants = tpl.getElementsByTagName(XmlFilePlugin.ELEM_AST);
int len = descendants.getLength();
for (int i = 0; i < len; i++)
{
Element next = (Element) descendants.item(i);
if (next.hasAttribute(XmlFilePlugin.ATTR_ID))
{
next.setAttribute(XmlFilePlugin.ATTR_ID, "0");
}
}
}
}
/**
* Context local work area.
*/
private static class WorkArea
{
/** Temporary storage for ASTs that are created but not yet attached. */
private final Map<String, Aast> depot = new HashMap<>();
}
}