package com.openMap1.mapper.writer;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.Vector;
import org.eclipse.core.resources.IFile;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EPackage;
import org.w3c.dom.Element;
import com.openMap1.mapper.core.ClassSet;
import com.openMap1.mapper.core.MDLWriteException;
import com.openMap1.mapper.core.MapperException;
import com.openMap1.mapper.core.NamespaceException;
import com.openMap1.mapper.core.NamespaceSet;
import com.openMap1.mapper.core.Xpth;
import com.openMap1.mapper.core.RunIssue;
import com.openMap1.mapper.core.namespace;
import com.openMap1.mapper.util.FileUtil;
import com.openMap1.mapper.util.GenUtil;
import com.openMap1.mapper.util.Timer;
import com.openMap1.mapper.util.XMLInputFile;
import com.openMap1.mapper.util.XMLOutputFile;
import com.openMap1.mapper.util.XMLUtil;
import com.openMap1.mapper.util.messageChannel;
import com.openMap1.mapper.MappedStructure;
/**
* superclass of classes that use wproc procedures - to
* execute them or generate XSLT from them.
*
* @author robert
*
*/
abstract public class ProcedureUser extends ProcedureClass {
// Hashtable of all procedures found to be missing at run time
protected Hashtable<String, String> missingProcedures;
// cache mappings for V3 data types to avoid reloading them many times
private Hashtable<String,ProcedureClass> dataTypeProcClasses = new Hashtable<String,ProcedureClass>();
/**
* All issues that were noted when running the translation
* or generating XSLT.
* outer key = string form of root path
* Inner key = a unique identifier for the issue
* @return
*/
public Hashtable<String,Hashtable<String,RunIssue>> allRunIssues() {return allRunIssues;}
protected Hashtable<String,Hashtable<String,RunIssue>> allRunIssues;
/** XML file for the output XML (result) */
protected XMLOutputFile xout() {return xout;}
protected XMLOutputFile xout; // XML output file
public void setXMLOutputFile(XMLOutputFile xout)
{
this.xout = xout;
setOutputFileInAllProcs(xout);
}
/** true for detailed trace of WPR code execution */
public boolean runTracing() {return runTracing;}
protected boolean runTracing; // if true, write trace output from running XW procedures
/** XML file (typically with extension .wpr) to read the XML writing procedures,
* if they have been compiled before */
public XMLInputFile xProcIn() {return xProcIn;}
protected XMLInputFile xProcIn;
//protected Element procsFileRoot;
/** gets objects, attributes and association instances from the input XML document */
public objectGetter oGet() {return oGet;}
protected objectGetter oGet; // gets object model data to be written out
protected boolean readXMLProcs = false;
protected IFile resultFile;
// dummies of methods not relevant to procedure users
public boolean codeTracing() {return false;}
protected void codeTrace(String s) {}
protected void writeXMLProc(WProc proc) {}
private boolean tracing = true;
//----------------------------------------------------------------------------------------
// constructor
//-----------------------------------------------------------------------------------------
/**
* constructor for use inside Eclipse
*/
public ProcedureUser(IFile procsFile, objectGetter oGet,
MappedStructure ms, messageChannel mChan,
IFile resultFile, boolean runTracing) throws MapperException
{
super(ms,mChan);
if ((procsFile != null) && (procsFile.exists())) try
{
xProcIn = new XMLInputFile();
xProcIn.readXMLFile(procsFile.getContents()); // this sets instance variable root
readXMLProcs = true;
}
catch (Exception ex)
{throw new MapperException("Failed to open write procedures file: " + ex.getMessage());}
// create empty output XML file
createXMLOutputFile();
this.oGet = oGet;
this.resultFile = resultFile;
this.runTracing = runTracing;
}
/**
* constructor for use outside Eclipse, in standalone applications
*/
public ProcedureUser(objectGetter oGet,
MappedStructure ms, EPackage classModel, messageChannel mChan,
boolean runTracing) throws MapperException
{
super(ms,classModel,mChan);
xProcIn = new XMLInputFile();
xProcIn.setRootElement(ms.procedureFileRoot());
readXMLProcs = true;
// create empty output XML file
createXMLOutputFile();
this.oGet = oGet;
this.runTracing = runTracing;
}
protected abstract void createXMLOutputFile() throws MapperException;
//--------------------------------------------------------------------------
// Reading in an XML form of the generation procedures
//--------------------------------------------------------------------------
/**
* read the WProc procedures from one or more supplied WProc files, and store them in
* Hashtable<String, Hashtable<String, WProc>> procedureTables (from ProcedureClass)
*
* @param mergeImports if true, wherever a WProc has a fillElement step invoking an imported Wproc file,
* merge in the WProcs from the imported file
* @param isXSLTGeneration: if true, the WProcs will be of class WProcXSLT
*/
public void readProcedures(boolean mergeImports, boolean isXSLTGeneration) throws MapperException
{
if (readXMLProcs)
{
if (xProcIn.root() == null)
{throw new MDLWriteException("Could not read file of write procedures.");}
else
{
Element topProcs = (Element)xProcIn.root();
if (!(xProcIn.getLocName(topProcs).equals("procedures")))
{throw new MDLWriteException("Root element is not a <procedures> element.");}
Xpth startPath = new Xpth(NSSet());
startPath.setFromRoot(true);
// read the startup procedure
Element startProcEl = XMLUtil.firstNamedChild(topProcs,"proc");
if (startProcEl != null)
{
WProc wp = new WProc(startPath,this,startProcEl);
if (isXSLTGeneration)
wp = new WProcXSLT(startPath,this,(XSLGenerator)this, startProcEl);
storeProcedure(wp,false);
}
else {message("No startup procedure in file");}
// recursive descent of XML tree to read all other procedures
Vector<String> wProcFiles = new Vector<String>();
Vector<String[]> elementClasses = null; // makes all element tag names pass the inclusion test
readProceduresInSubtree(topProcs,startPath,elementClasses,mergeImports,isXSLTGeneration,this,wProcFiles,null, null, null);
}
}
else
{throw new MDLWriteException("No XML file of write procedures has been specified");}
}
/**
*
* @param el the <element> element currently being analysed for its one or two WProcs
* @param pathToEl XPath from the root to this element
* @param elementClasses Vector of [tag name, className] pairs from the WProc for this element
*
* @param mergeImports if true, when fillElement steps are encountered,
* expand the referenced WProc file in line
* @param isXSLTGeneration if true, store WProxXSLT objects
* @param PC procedureClass to be used in WProc constructors, so they have the right output mappings
* @param wProcFiles when mergeImports = true, the stack of wproc files read so far,
* to avoid infinite self-recursion
* @param outerWProc when mergeImports = true, the WProc which made this import, from which when-values must be copied
* @param oldCSet when mergeImports = true, the classSet to be replaced by the parameter classSet in all steps
* @param newCSet when mergeImports = true, the parameter classSet to replace the old one in all steps
* @throws MapperException
*/
private void readProceduresInSubtree(Element el, Xpth pathToEl,
Vector<String[]> elementClasses,
boolean mergeImports,boolean isXSLTGeneration,
ProcedureClass PC,
Vector<String> wProcFiles, WProc outerWProc,
ClassSet oldCSet, ClassSet newCSet) throws MapperException
{
Vector<Element> elements = XMLUtil.namedChildElements(el,"element");
for (int i = 0; i < elements.size(); i++)
{
Element child = (Element)elements.elementAt(i);
String tagName = child.getAttribute("name");
// only include the WProc for this child if its class is represented in the input (for XSLT generation)
if (includeThisChild(tagName,elementClasses))
{
Xpth nextPath = pathToEl.addInnerStep(tagName);
// prepare to record which classes are required to be represented to populate each element of the child element's children
Vector<String[]> nextElementClasses = new Vector<String[]>();
Element procsEl = XMLUtil.firstNamedChild(child,"procedures");
if (procsEl != null)
{
Vector<Element> procs = XMLUtil.namedChildElements(procsEl,"proc");
for (int j = 0; j < procs.size(); j++)
{
Element procEl = (Element)procs.elementAt(j);
// if this WProc imports another WProc file, and imported WProc files are to be merged, do so
if ((mergeImports) && (fillElementStep(procEl) != null))
{
// garbage collect
Runtime.getRuntime().gc();
doMergedImports(procEl,nextPath, isXSLTGeneration, wProcFiles, oldCSet, newCSet);
}
// in all other cases, store the WProc (and increment the Vector nextElementClasses)
else
{
readOneProcedure(procEl, nextPath, nextElementClasses,mergeImports, isXSLTGeneration, PC, outerWProc, oldCSet, newCSet);
}
}
}
// recursive step to child elements introduced in the same WProc file
readProceduresInSubtree(child,nextPath, nextElementClasses,mergeImports,isXSLTGeneration, PC, wProcFiles,outerWProc, oldCSet, newCSet);
}
}
}
/**
* @param tagName the tag name of a child element
* @param elementClasses a Vector of [tag name,class name] pairs
* @return true if the child tag name is in the list, with a class that is represented in the input;
* or with class name "" from an AddElement step
* or true if the Vector is null (as it is on the top call to readProceduresInSubtree)
*/
private boolean includeThisChild(String tagName,Vector<String[]>elementClasses)
{
if (elementClasses == null) return true;
boolean include = false;
for (int i = 0; i < elementClasses.size(); i++)
{
String[] group = elementClasses.get(i);
if (group[0].equals(tagName))
{
// element introduced by AddElement step; no test
if (group.length == 1) include = true;
// element introduced by AllObjects step; test class is mapped
else if (group.length == 2)
include = filterbyDoubleClassMappings(group[1]);
// element introduced by AssociationInstance step; test association is mapped
else if (group.length == 4)
include = filterbyInputAssociationMappings(group[1],group[2],group[3]);
}
}
return include;
}
/*
* ProcedureUser does not have access to the input mappings for XSLT generation, so cannot filter
* .wproc files on that basis. This method is overridden by one in XSLGenerator which does filter
*/
protected boolean filterbyDoubleClassMappings(String className)
{
return true;
}
/*
* ProcedureUser does not have access to the input mappings for XSLT generation, so cannot filter
* .wproc files on that basis. This method is overridden by one in XSLGenerator which does filter
*/
protected boolean filterbyInputAssociationMappings(String class1,String class2,String assocName)
{
return true;
}
/**
*
* @param procEl <proc> element whose contents define the WProc
* @param nextPath path to the element of the WProc
* @param elementClasses Vector of [tag name, class name] for shle elements; to be added to
* @param mergeImports true if imported .wproc files from fillElement steps are to be merged in
* @param isXSLTGeneration true for XSLT generation
* @param outerWProc used to get further when values from
* @param oldCSet classSet to be substituted
* @param newCSet classSet to substitute it with
* @throws MapperException
*/
private WProc readOneProcedure(Element procEl, Xpth nextPath, Vector<String[]> elementClasses, boolean mergeImports, boolean isXSLTGeneration,
ProcedureClass PC,
WProc outerWProc,ClassSet oldCSet, ClassSet newCSet) throws MapperException
{
WProc wp = new WProc(nextPath,PC,procEl);
if (isXSLTGeneration)
wp = new WProcXSLT(nextPath,PC,(XSLGenerator)this,procEl);
if (mergeImports)
{
if (newCSet != null) wp.replaceClassSet(oldCSet, newCSet);
if (outerWProc != null) wp.takeWhenValues(outerWProc);
}
storeProcedure(wp,false);
// add to the list of [tag name, class] pairs
Vector<String[]> newPairs = wp.tagConditions();
for (int i = 0; i < newPairs.size(); i++) elementClasses.add(newPairs.get(i));
return wp;
}
/**
* @param procEl an element defining a WProc
* @return if the described WProc has one 'fillElement' step (importing another WProc file),
* the Element defining the step, which must be its last step;
* or null if there is no FillElement step in the WProc.
*
*/
private Element fillElementStep(Element procEl) throws MapperException
{
Element fillElement = null;
Vector<Element> steps = XMLUtil.namedChildElements(procEl,"step");
for (int i = 0; i < steps.size(); i++)
{
Element el = steps.get(i);
boolean isFill = el.getAttribute("stepType").equals("fillElement");
if (isFill && (i < steps.size() -1))
throw new MapperException("FillElement step can only be the last step of the WProc for element '"
+ procEl.getAttribute("name") + "'");
if (isFill) fillElement = el;
}
return fillElement;
}
/**
* @param procEl the <proc> element containing a fillElement step
* @param pathToEl XPath from the root to this element
*
* @param isXSLTGeneration if true, store WProxXSLT objects rather than just WProc objects
* @param wProcFiles when mergeImports = true, the stack of .wproc files read so far,
* to avoid infinite self-recursion
* @param oldCSet the previous classSet to be replaced by the parameter classSet in all steps
* @param newCSet the previous parameter classSet to replace the old one in all steps
* @throws MapperException
*/
private void doMergedImports(Element procEl, Xpth pathToEl,
boolean isXSLTGeneration,
Vector<String> wProcFiles,
ClassSet oldCSet, ClassSet newCSet) throws MapperException
{
/* make the WProc which made the import (to hand on its when values,
* and append any other steps to the topw WProc of the imported file) */
WProc outerWProc = new WProc(pathToEl,this,procEl);
if ((oldCSet != null) && (newCSet != null))
outerWProc.replaceClassSet(oldCSet, newCSet);
// extract the fillElement step from the XML
Element step = fillElementStep(procEl);
// calculate the ClassSet to substitute in the imported WProcs, from the importing WProc
ClassSet nextNewCSet = new ClassSet(step.getAttribute("className"),step.getAttribute("subset"));
// if there are no input mappings to this class, stop the recursion
if (!filterbyDoubleClassMappings(nextNewCSet.className()))
{
storeProcedure(outerWProc,false);
return;
}
// location of the imported mapper file (& determines location of the imported wproc file)
String mapperFileLocation = step.getAttribute("mapperPath");
/* guard against infinite self-recursion; and calculate the new Vector to pass on in recursion
* Allow any .wproc file to be used up to 3 times */
if (GenUtil.countOccurrences(mapperFileLocation, wProcFiles) > 2)
{
storeProcedure(outerWProc,false);
return;
}
Vector<String> newProcFiles = new Vector<String>();
for (int i = 0; i < wProcFiles.size(); i++) newProcFiles.add(wProcFiles.get(i));
newProcFiles.add(mapperFileLocation);
/* try to find the imported ProcedureClass (for the output mappings)
* and the root element of the imported WProc file */
Element importRoot = null;
String procLocation = "";
ProcedureClass importPC = null;
try
{
importPC = getProcedureClass(mapperFileLocation);
importPC.setRunTracing(this.runTracing());
procLocation = FileUtil.wProcLocation(mapperFileLocation);
URI uri = URI.createURI(procLocation,true);
String location = FileUtil.removeFilePrefix(FileUtil.editURIConverter().normalize(uri).toString()); // strip off 'file:/'
importRoot = XMLUtil.getRootElement(location);
if (importRoot == null) throw new MapperException("Cannot find imported WProc file at '" + procLocation + "'");
}
// if the imported WProc file cannot be found, terminate the recursion safely
catch (Exception ex)
{
System.out.println(ex.getMessage());
storeProcedure(outerWProc,false);
return;
}
if ((oldCSet != null) &&(nextNewCSet.equals(oldCSet))) nextNewCSet = newCSet; // if this classSet is already in a ClassSet substitution
// find the parameter classSet to be replaced in all WProcs of this .wproc file
Element impProc = XMLUtil.firstNamedChild(importRoot, "proc");
Element impStep = XMLUtil.firstNamedChild(impProc, "step");
if (!impStep.getAttribute("stepType").equals("getAllObjects"))
throw new MapperException("Unexpected first step in top WProc of imported wproc file at '" + procLocation + "'");
ClassSet nextOldCSet= new ClassSet(impStep.getAttribute("class"),impStep.getAttribute("subset"));
// find the top <element> element of the imported WProc file, and the <proc> element inside it
Element topImportElement = XMLUtil.firstNamedChild(importRoot, "element");
if (topImportElement == null)
throw new MapperException("No top <element> element in imported wproc file at '" + procLocation + "'");
Element importProcs = XMLUtil.firstNamedChild(topImportElement, "procedures");
Element importProc = XMLUtil.firstNamedChild(importProcs, "proc");
if (importProc == null) throw new MapperException("No top WProc in imported wproc file at '" + procLocation + "'");
// read the top WProc of the imported .wproc file, with mergeImports = true.
Vector<String[]> elementClasses = new Vector<String[]>();
WProc topImportProc = readOneProcedure(importProc, pathToEl, elementClasses,true,
isXSLTGeneration, importPC, outerWProc, nextOldCSet, nextNewCSet);
// append at the front any steps from the importing WProc, except its FillElement step
topImportProc.preAppendSteps(outerWProc);
// carry on the recursion in the imported .wproc file, storing WProcs, with mergeImports = true.
readProceduresInSubtree(topImportElement,pathToEl,elementClasses, true,isXSLTGeneration,
importPC, newProcFiles, outerWProc,nextOldCSet, nextNewCSet);
}
/**
* retrieve procedure classes (sets of output mappings)
* caching the mappings for V3 data type classes
*/
private ProcedureClass getProcedureClass(String mapperFileLocation) throws MapperException
{
boolean isDT = isV3DataTypeMappingSet(mapperFileLocation);
ProcedureClass importPC = null;
// using private Hashtable<String,ProcedureClass> dataTypeProcClasses = new Hashtable<String,ProcedureClass>();
// if this is a set of V3 data type mappings, try to get it from the cache
if (isDT) importPC = dataTypeProcClasses.get(mapperFileLocation);
if (importPC == null)
{
MappedStructure importMS = FileUtil.getImportedMappedStructure(ms(), mapperFileLocation);
if (importMS == null) throw new MapperException("Cannot find imported output mappings at '" + mapperFileLocation + "'");
importPC = new ProcedureClass(importMS, mChan());
// if this is a new set of V3 data type mappings, store it in the cache
if (isDT) dataTypeProcClasses.put(mapperFileLocation,importPC);
// trace of memory usage
System.out.println("Import mappings at " + mapperFileLocation);
GenUtil.writeMemory();
}
return importPC;
}
private boolean isV3DataTypeMappingSet(String mapperFileLocation)
{
boolean isV3 = false;
StringTokenizer st = new StringTokenizer(mapperFileLocation,"/");
while (st.hasMoreTokens()) if (st.nextToken().equals("V3DataTypes")) isV3 = true;
return isV3;
}
/**
* return the top element of the Domain Object Model (DOM)
* of the output XML.
*/
public Element outputDOM()
{
return xout.topOut();
}
//--------------------------------------------------------------------------
// writing XML Output
//--------------------------------------------------------------------------
/** execute all procedures to create output XML,
starting from the start procedure*/
public void executeProcedures(boolean runTracing) throws MapperException
{
timer.start(Timer.EXECUTE_PROCEDURES);
//housekeeping
if (oGet == null) throw new MDLWriteException("Null objectGetter for OXWriter");
allRunIssues = new Hashtable<String,Hashtable<String,RunIssue>>() ;
missingProcedures = new Hashtable<String, String>();
this.runTracing = runTracing;
// do the action
boolean isXSLT = false;
WProc startProc = getStartProcedure(isXSLT);
startProc.executeProcedure(allRunIssues);
// more housekeeping
writeMissingProcedures();
timer.stop(Timer.EXECUTE_PROCEDURES);
}
/// return the start procedure, with the correct start context
public WProc getStartProcedure(boolean isXSLT) throws MapperException
{
WProc startProc = null;
Xpth startPath = new Xpth(NSSet());
Hashtable<String,WProc> sp = procedureTables.get(startPath.stringForm());
// there is just one (start) procedure for the empty path
for (Enumeration<WProc> en = sp.elements(); en.hasMoreElements();)
{startProc = ((WProc)en.nextElement()).pClone();}
if (startProc == null) {throw new MDLWriteException("Failed to find start procedure");}
startProc.setContext(getStartContext());
startProc.giveTimer(timer, false);
return startProc;
}
subtreeContext getStartContext() throws MDLWriteException
{
Xpth startPath = new Xpth(NSSet());
subtreeContext context = new subtreeContext(this,startPath,oGet,this);
return context;
}
/* find a procedure which matches the root path, the create/revisit flag, and the
when-condition values of the subtree context.
Write an error message if none can be found.
Then execute it. */
boolean callProcedure(int timerToStop,Element el, boolean createdElement, Xpth newPath,
subtreeContext context, Hashtable<String,Hashtable<String,RunIssue>> runIssues)
throws MapperException
{
timer.start(Timer.CALL_PROCEDURES);
/* stop the timer of the calling step, so that it does not double-count
* what is counted in the steps of this procedure */
timer.stop(timerToStop);
boolean result = false;
runTrace("Call depth: " + newPath.size());
WProc runProc = findProcedure(createdElement,newPath,context);
if (runProc != null)
{
// pass the runtime environment to it
runProc.setContext(context);
runProc.setCurrentElement(el);
runProc.setRootPath(newPath);
runProc.setOutputFile(xout);
// execute it
timer.stop(Timer.CALL_PROCEDURES);
result = runProc.executeProcedure(runIssues);
}
// You should always find a create procedure, but it is OK not to find a revisit procedure
else if (createdElement)
{
throw new MDLWriteException("Amongst the procedures for root path "
+ newPath.stringForm() + " none is appropriate.");
}
else if (!createdElement) {result = true;}
// restart the timer of the calling step
timer.start(timerToStop);
return result;
}
public WProc findProcedure(boolean createdElement, Xpth newPath, subtreeContext context)
throws MapperException
{
timer.start(Timer.FIND_PROCEDURES);
Hashtable<String, WProc> candidates = null;
String tryPathString;
Xpth tryPath,bestPath = null;
WProc runProc = null;
boolean found = false;
/* normal case: XPaths are definite, so there are some WProcs stored under
* exactly this XPath */
if (procedureTables.get(newPath.stringForm()) != null) bestPath = newPath;
/* otherwise, if procedures are stored with indefinite paths,
* find the vector of candidate procedures
with the most specific path specification matching the root path */
else for (Enumeration<String> en = procedureTables.keys();en.hasMoreElements();)
{
tryPathString = en.nextElement();
tryPath = new Xpth(newPath.NSSet(),tryPathString);
if ((tryPath.compatible(newPath)) && (tryPath.asSpecificAs(bestPath)))
{bestPath = tryPath;}
}
if (bestPath == null) // this exception will be trapped and the path stored for a later message
{throw new MDLWriteException("Cannot find any WProc for path '" + newPath.stringForm() + "'");}
else
{
candidates = procedureTables.get(bestPath.stringForm());
// exhaustive search of all candidates to find which one matches the when-values and create/revisit flag
for (Enumeration<WProc> en = candidates.elements(); en.hasMoreElements();)
{
WProc template = en.nextElement();
if ((template.onCreate() == createdElement) && (template.matchWhenValues(context)))
{
if (found) {throw new MDLWriteException("Found more than one matching procedure for root path " + newPath.stringForm());}
// make a new runtime copy of the procedure
else {runProc = template.pClone();}
found = true;
}
}
}
if (runProc == null) {throw new MapperException("No write procedure matched at path '" + newPath.stringForm() + "'");}
runProc.giveTimer(timer, false);
timer.stop(Timer.FIND_PROCEDURES);
return runProc;
}
void runTrace(String s) {if (runTracing) message(s);}
/** set the namespaces in the output XML file to be
* the same as those in the output structure definition, in both prefix and URI
* - except we do not want the XML Schema namespace in the output namespaces. */
public void setOutputNamespaces() throws NamespaceException
{
xout.setNSSet(new NamespaceSet());
for (int i = 0; i < NSSet().size(); i++)
{
namespace ns = NSSet().getByIndex(i);
if (!(ns.URI().equals(XMLUtil.SCHEMAURI))) xout.NSSet().addNamespace(ns);
}
}
// store missing procedures without duplicates.
public void recordMissingProcedure(String pathString)
{missingProcedures.put(pathString,"1");}
// write a list of missing procedures, if there were any.
private void writeMissingProcedures()
{
if (missingProcedures.size() > 0)
{
message("Procedures for the following nodes were missing: ");
for (Enumeration<String> en = missingProcedures.keys(); en.hasMoreElements();)
{
String procPath = en.nextElement();
message(procPath);
}
}
}
@SuppressWarnings("unused")
private void trace(String s) {if (tracing) System.out.println(s);}
}