package org.ngbw.pise.commandrenderer;
import java.io.IOException;
import java.io.StringBufferInputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.ngbw.sdk.api.tool.CommandRenderer;
import org.ngbw.sdk.common.util.StringUtils;
import org.ngbw.sdk.common.util.SuperString;
import org.ngbw.sdk.tool.RenderedCommand;
import org.ngbw.pise.commandrenderer.PiseMarshaller;
import org.ngbw.sdk.api.tool.FieldError;
import org.ngbw.sdk.api.tool.JobValidationException;
/**
* This Class implements CommandRenderer this implementation takes care of
* PiseXML and Perl contents
*
* @author Rami
*
* @author R. Hannes Niedner
*
*/
public class PiseCommandRenderer implements CommandRenderer
{
private static Log log = LogFactory.getLog(PiseCommandRenderer.class .getName());
private final Map cfgMap = new HashMap();
private PiseMarshaller piseMarshaller;
//parameter name -> parameter value
private Map parameters;
// private variable for the toUnixCmd() method
private String[] unixCmdGroup = null;
private String[] unixCmdGroupNegative = null;
private Map paramFiles = null;
private RenderedCommand renderedCommand= null;
private final static String SCHEDULER_CONF = "scheduler.conf";
private PerlEval perlEval = null;
List parameterErrors = new ArrayList();
public PiseCommandRenderer()
{
super();
init();
}
private void init()
{
parameters = null;
piseMarshaller = null;
unixCmdGroup = new String[100];
unixCmdGroupNegative = new String[100];
paramFiles = new HashMap();
renderedCommand = new RenderedCommand();
}
// TODO:
public void validate(URL url, Map parameters)
{
render(url, parameters, true);
}
public RenderedCommand render(URL url, Map parameters)
{
return render(url, parameters, false);
}
/**
@param url Url of the pise xml file
@param parameters A map of parameter name to value, where parameter name is the name element
of a parameter element in the pise xml file and where value may be something like
"1" or "y", or the contents of a source document, depending on the type of parameter.
Caller must verify that entries in parameters map meet certain criteria BEFORE
calling this method:
- keys must correspond to parameters in the pise xml file
- values of the correct type as specified by the pise file (for example, integer, string, double)
- values are in min/max range specified by the pise xml
This method does evaluates perl precond and ctrls elements for the parameters and throws
JobValidationException if they are violated. If precond for a parameter is not met, the parameter must not be
in the parameters map.
*/
private RenderedCommand render(URL url, Map parameters, boolean validateOnly)
{
try
{
log.debug("render command from " + parameters.size() + " parameters using config:" + url);
log.debug("validateOnly is " + validateOnly);
init();
/*
Keep a pointer to the passed in parameters map. This method will modify the map -
upon return, caller will see a modified map. For example, this method adds dummy
parameters that correspond to the parameter files that are generated by the pisexml.
*/
this.parameters = parameters;
piseMarshaller = initPiseMarshaller(url);
perlEval = new PerlEval();
perlEval.initialize();
List commandList = toUnixCmd(validateOnly);
perlEval.terminate();
if (validateOnly)
{
return null;
}
//log.debug("toUnixCmd returns a list with " + commandList.size() + " elements");
String[] commandArray = new String[commandList.size()];
renderedCommand.setCommand(commandList.toArray(commandArray));
if (log.isDebugEnabled())
{
log.debug("Returned command: " + StringUtils.join(renderedCommand.getCommand(), " "));
Map input = renderedCommand.getInputFileMap();
Map output = renderedCommand.getOutputFileMap();
for(String parameter : input.keySet())
log.debug("Input: " + parameter + ": " + input.get(parameter));
for(String parameter : output.keySet())
log.debug("Output: " + parameter + ": " + output.get(parameter));
}
setSchedulerProperties();
return renderedCommand;
}
catch(JobValidationException jve)
{
throw jve;
}
catch (Exception err)
{
throw new RuntimeException(err);
}
finally
{
if (perlEval != null)
{
perlEval.cleanup();
}
}
}
/**
If the xml specified a param file with a specific name (given by SCHEDULER_CONF)
we expect that file to contain properties for scheduling the job. We load the contents
of the file into the renderedCommand.schedulerProperties.
*/
private void setSchedulerProperties()
{
Map inputData = new TreeMap();
Map inputFileNames = renderedCommand.getInputFileMap();
String p = null;
for (Iterator> names = inputFileNames.entrySet().iterator() ; names.hasNext() ; )
{
Map.Entry entry = names.next();
String parameter = entry.getKey();
String fileName = entry.getValue();
if (fileName.equals(SCHEDULER_CONF))
{
log.debug("Found " + SCHEDULER_CONF + " in renderedCommand.inputFileMap, for parameter: " + parameter);
p = parameter;
break;
}
}
String value;
if (p == null || (value = parameters.get(p)) == null)
{
log.debug("Parameter " + p + " value is null.");
return;
}
log.debug("Parameter " + p + " value is:" + new String(value));
try
{
renderedCommand.getSchedulerProperties().load(new StringBufferInputStream(value));
}
catch (Throwable t)
{
log.warn("Error loading scheduler properties", t);
}
}
//initialize the JAXB Marshaller
private PiseMarshaller initPiseMarshaller(URL url)
{
if (url == null)
throw new NullPointerException("Tool config file URL is null!");
if (cfgMap.containsKey(url) == false)
try
{
PiseMarshaller pm = new PiseMarshaller(url.openStream());
cfgMap.put(url, pm);
} catch (IOException e)
{
throw new RuntimeException("Cannot initialize PiseMarshaller.", e);
}
return cfgMap.get(url);
}
//all parameter names in the submitted maps have an underscore appended
//thus we declare our own private getters and setters for the value
private Set getParameterSet()
{
Set keySet = new HashSet();
for (String key : parameters.keySet())
{
//the regex secefies the only a trailing _
//and the replaceFirst will only take this one
String paramName = key.replaceFirst("_$", "");
keySet.add(paramName);
}
return keySet;
}
private String getParameterKey(String parameter)
{
return parameter + "_";
}
private String getParameterValue(String parameter)
{
String key = getParameterKey(parameter);
if (parameters.containsKey(key) || parameters.get(key) != null)
return new String(parameters.get(key));
else
return null;
// that is expected since parameters with
// null values are notin the parameter map
}
private void setParameterValue(String parameter, String value)
{
String key = parameter + "_";
parameters.put(key, value);
}
private void setInputFileName(String parameter, String fileName)
{
String key = parameter + "_";
renderedCommand.getInputFileMap().put(key, fileName);
}
private void setOutputFileName(String parameter, String fileName)
{
renderedCommand.getOutputFileMap().put(parameter, fileName);
}
private String evaluatePerlStatement(String perlStatement) throws Exception
{
return perlEval.evaluateStatement(perlStatement);
}
/*
private String evaluatePerlStatement(String perlStatement) throws IOException, InterruptedException, ExecutionException
{
log.debug("PerlStatement: " + perlStatement);
// after some test, we had to create a table containing the different element of
// the statement, otherwise it seems not working with one String
String[] command = new String[3];
command[0] = "perl";
command[1] = "-e";
command[2] = perlStatement;
Process worker = Runtime.getRuntime().exec(command);
final Future stdOut = InputStreamCollector.readInputStream(worker.getInputStream());
final Future stdErr = InputStreamCollector.readInputStream(worker.getErrorStream());
final int exitCode = worker.waitFor();
if (exitCode != 0)
throw new RuntimeException("Exit value was not 0 but " + exitCode + " STDERR: \n" + stdErr.get());
return stdOut.get().trim();
}
*/
/*
*/
private List toUnixCmd(boolean validateOnly) throws IOException, InterruptedException, ExecutionException, Exception
{
List commandList = new ArrayList();
/*
A command is constructed from multiple type of parameters:
These are the parameters passed as Map argument for this class, which means parameter selected through GUI
*/
log.debug("toUnixCmd: processing " + getParameterSet().size() + " GUI parameters");
FieldError error;
for (String paramName : getParameterSet())
{
processParameter(paramName, false);
}
if (parameterErrors.size() > 0)
{
throw new JobValidationException(parameterErrors);
}
if (validateOnly)
{
return null;
}
// These are the parameters hidden from the GUI, but necessary for generating the command line correctly
log.debug("toUnixCmd: processing " + piseMarshaller.getHiddenSet().size() + " HIDDEN parameters");
for (String paramName : piseMarshaller.getHiddenSet())
processParameter(paramName, true);
/*
These are the parameters that generate an outfile, they are hidden from the GUI since the GUI dosen't
give the user the possibility to specify the names of the output files.
*/
log.debug("toUnixCmd: processing " + piseMarshaller.getOutfileSet().size() + " OUTFILESET parameters");
for (String paramName : piseMarshaller.getOutfileSet())
processParameter(paramName, false);
// Not sure of the difference bewteen Pise ResultFile and piseOutfile, but here we handle ResultFiles.
log.debug("toUnixCmd: processing " + piseMarshaller.getResultSet().size() + " RESULTSET parameters");
for (String paramName : piseMarshaller.getResultSet())
processParameter(paramName, true);
/*
All kinds of parameters, visible, hidden, etc can specify text to be placed in a parameter
file instead of on the commandline. The calls to processParameter above put entries in
paramFiles each time they find a parameter that puts text in a parameter file. The map is keyed by
parameter file name. Here we append all the lines that go into each parameter file into a single
string and we add the param file name/ param file contents to our list of input files.
*/
for(String key : paramFiles.keySet())
{
// Append the lines that go in the parameter file.
String[] values = paramFiles.get(key);
StringBuilder valueSb = new StringBuilder();
for(String value : values)
{
if (value != null)
{
valueSb.append(value);
}
}
String value = valueSb.toString();
/*
This adds an entry in the parameter map, with key = parameter file name, value = contents.
See NEWLINE comment at end of file.
*/
setParameterValue(key, value.replace("\\n", "\n"));
/*
This adds an entry in the input file map with key = parameter file name, value = parameter file name.
*/
setInputFileName(key, key);
}
// 3- Creating the command line by ordering them with respect to their group
if (unixCmdGroup[0] != null)
{
commandList.add(unixCmdGroup[0]);
log.debug("for unixCmdGroup[0] commandList.add: " + unixCmdGroup[0]);
}
for (int j = 1; j < unixCmdGroup.length; j++)
{
// Arranging positve groups
if (unixCmdGroup[j] != null)
{
commandList.add(unixCmdGroup[j]);
log.debug("for unixCmdGroup[" + j + "] commandList.add: " + unixCmdGroup[j]);
}
}
// Arranging the negative groups
for (int k=1 ; k controls = piseMarshaller.getCtrl(paramName);
if (parameterValue == null || (parameterValue.length() < 100))
{
log.debug(paramName +"=" + parameterValue + ", type=" + type);
} else
{
log.debug(paramName +"=" + parameterValue.substring(0, Math.min(100, parameterValue.length())) +
"...(truncated), type=" + type);
}
@SuppressWarnings("unused")
String isCommand = piseMarshaller.getIsCommand(paramName);
@SuppressWarnings("unused")
String ishidden = piseMarshaller.getIsHidden(paramName);
// With flist, user chooses a label that pise maps to a string of code.
String flist = piseMarshaller.getflistValue(paramName, parameterValue);
String perlFormat = null;
String perlPrecond = null;
if (type.equals("Results") && group == null)
{
group = "-99"; // this group is not going to be used at all
}
log.debug("Group: " + group);
int groupValue=1;
if (group !=null)
{
groupValue = Integer.parseInt(group);
}
/*
here we assume that flist and format are mutually exclusive however the case where we have a format and a flist,
the flist will override format I don't see the sense of having both in the same parameter
*/
if (flist != null)
{
format = flist;
}
/*
The front end code is responsible for inserting the separator character, if any,
and concatenating the list elements. However, if the pise doesn't specify a separator,
the front end uses "@" as a separator because it must maintain the distinct elements in order
to repopulate a form from them. THIS MEANS YOU CAN'T USE AN '@' in a LIST ELEMENT.
*/
if (type != null && type.equals("List"))
{
if (separator == null)
{
// Remove the dummy '@' that the front end inserted and concatenate the elements since there
// is not separator specified in the pise xml.
setParameterValue(paramName, SuperString.valueOf(parameterValue, '@').concatenate());
} else
{
;
}
}
// Todo: If precondition is present and is false skip it. Would be nice to return a warning.
// Not an error, because portal send values for all fields with default values.
log.debug("precond: " + precond);
if (precond != null)
{
precond = restitutionPrecond(precond, paramName, vdef);
perlPrecond = evaluatePerlStatement(precond);
log.debug("Precond evaluation: " + perlPrecond + ", boolean value is " + Boolean.valueOf(perlPrecond));
if (!Boolean.valueOf(perlPrecond))
{
//parameterErrors.add(new FieldError(paramName, "Precondition not satisfied."));
log.debug("Precondition not satisfied, not generating code for " + paramName);
return;
}
}
if (evaluateControls(controls, paramName, vdef) == false)
{
// If controls are violated quit processing this parameter
log.debug("Return early because evaluateControls returned false");
return;
}
log.debug("format: " + (format == null ? "null" : format));
if (format != null)
{
format = restitutionFormat(format, paramName, vdef);
perlFormat = evaluatePerlStatement(format);
log.debug("Format evaluation: " + perlFormat);
}
if (perlFormat != null)
{
log.debug("paramfile: " + (paramfile == null ? "null" : paramfile));
if (paramfile != null)
{
String[] value;
if (paramFiles.containsKey(paramfile) == false || paramFiles.get(paramfile) == null)
{
paramFiles.put(paramfile, new String[100]);
}
value = paramFiles.get(paramfile);
if (value[groupValue] != null)
{
value[groupValue] += perlFormat;
}
else
{
value[groupValue] = perlFormat;
}
log.debug("parameter: " + paramName + " put in inputDataMap: '" + paramfile + "' -> '" + value[groupValue] + "'");
} else
{
if (groupValue >= 0)
{
if (unixCmdGroup[groupValue] != null)
{
unixCmdGroup[groupValue] += " " + perlFormat;
}
else
{
unixCmdGroup[groupValue] = perlFormat;
}
log.debug("unixCmdGroup[" + groupValue + "] = " + unixCmdGroup[groupValue]);
} else
{
if (unixCmdGroupNegative[Math.abs(groupValue)] != null)
{
unixCmdGroupNegative[Math.abs(groupValue)] += " " + perlFormat;
}
else
{
unixCmdGroupNegative[Math.abs(groupValue)] = perlFormat;
}
log.debug("unixCmdGroupNegative[" + Math.abs(groupValue) + "] = " + unixCmdGroupNegative[Math.abs(groupValue)]);
}
}
}
if (filenames != null)
{
// filenames not null for infile and sequence means we have assigned standard names for these input files
if (type.equals("InFile") || type.equals("Sequence"))
{
setInputFileName(paramName, filenames);
log.debug("setInputFileName: '" + paramName + "' -> '" + filenames + "'");
}
// Most output filenames are given in Results but can also be defined in Switch, List, OutFile, etc.
else
{
setOutputFileName(paramName, filenames);
log.debug("setOutputFileName: '" + paramName + "' -> '" + filenames + "'");
}
}
log.debug("END : processParameter: " + paramName + " (hidden: " + hidden + ")\n");
}
/*
TL: I'm adding this method to work around the problem where we want to know whether
the user uploaded a file, or pasted in some text, by checking to see if $param_name
is in the map. The problem with doing something like "($param_name)?...:..." is
that the restitution replaces $param_name with the contents of the file or pasted in
text, and if that contains a double quote or other character that has meaning to perl,
the resulting perl expression may be garbage. Instead we can say "(defined $param_name) ?
...:..." and this method will replace the conditional with 1 or 0.
- replaces "defined $value" with 1 (since in swami, $value is always defined)
- replaces "defined $vdef" with 1 if there is a vdef attribute in the pise xml element, 0 otherwise
- replaces "defined $param_name" with 1 if param_name is in the map of params we received from the gui,
0 otherwise.
*/
private String replaceDefined(String str, String parameterValue, String vdef)
{
StringBuffer buf = new StringBuffer();
// Find the text "defined", followed by whitespace, followed by "$", followed by one or more
// "word" characters (i.e letter, number or underscore). Capture the "word" into group 1.
Pattern p = Pattern.compile("defined\\s\\$(\\w+)");
Matcher matcher = p.matcher(str);
while(matcher.find())
{
/*
replacement text depends on the word that follows the $, that is, group(1).
but we'll be replacing the full string that was matched, in other words "defined $something"
will be replace with "0" or "1" depending on what $something is.
*/
String var = matcher.group(1);
if (var.equals("value"))
{
matcher.appendReplacement(buf, (parameterValue == null) ? "0" : "1");
} else if (var.equals("vdef"))
{
matcher.appendReplacement(buf, (vdef == null) ? "0" : "1");
} else
{
boolean defined = getParameterSet().contains(var);
matcher.appendReplacement(buf, defined ? "1": "0");
}
}
matcher.appendTail(buf);
return buf.toString();
}
/*
Returns a perl statement which will be passed to "perl -e". The stdout from
running perl -e on the statement will produce the text "true" or "false". We pass
the stdout to java's Boolean.valueOf() to get a boolean evaluation of the precond.
*/
private String restitutionPrecond(String precond, String paramName, String vdef)
{
/*
find every substring in precond starting with $ if the substring is $value replace it with the value of
the parameter identified in paramMap with key if the substring is $x replace it with the value of the
parameter identified in paramMap with x
*/
log.debug("Original Precondition: " + precond);
String paramValue = getParameterValue(paramName);
precond = replaceDefined(precond, paramValue, vdef);
log.debug("Precondition after replaceDefined: " + precond);
Pattern p = Pattern.compile("\\$\\w*");
Matcher m = p.matcher("Precond: " + precond);
while (m.find())
{
if (m.group().contains("$value"))
{
precond = precond.replace("$value", "\"" + paramValue + "\"");
}
else if (m.group().contains("$vdef"))
{
precond = precond.replace("$vdef", "\"" + vdef + "\"");
}
else
// we should ignore restitution if the pattern match $ preceding bug restituted $ by "null"
if (m.group().equalsIgnoreCase("$") == false)
{
String myKey = m.group().substring(1);
/*
In the special case where the code references $param_name and param_name isn't
in our map, we return "print ''", which when run, outputs nothing, which evaluates
to false.
*/
// Terri: I'm just trying out a change here. Up until now, if a parameter wasn't
// in the map we received and it was referenced in a precond, we didn't use the vdef
// value, we printed "" and bailed out, thus rendering the precondition false. I'm
// trying out a change where we instead using the vdef value. I'm hoping this makes
// it possible to avoid having to pass all the default values in the parameter map - that
// seems silly, but who knows what this change may break.
//###
String theValue;
if (getParameterSet().contains(myKey) == false)
{
theValue = piseMarshaller.getVdef(myKey);
if (theValue == null) // no vDef element in the pise xml
{
return "print \"\"";
}
// sometimes users specify vdef empty string as "" instead of just leaving the vdef element empty.
// Todo: fix this in the piseMarshaller?
if (theValue.equals("\"\""))
{
theValue = "";
}
} else
{
theValue = getParameterValue(myKey);
}
precond = precond.replace(m.group(), "\"" + theValue + "\"");
}
}
//In perl ("false")? print"true" : print "false" returns true so we are replacing all true by 1 and all false by 0
precond = precond.replaceAll("false", "0");
precond = precond.replaceAll("true", "1");
// precond returns a True or False depends on the condition being verified
precond = "(" + precond + ")? print \"true\" : print \"false\";";
log.debug("Restituted Precondition: " + precond);
return precond;
}
private String restitutionFormat(String format, String paramName, String vdef)
{
log.debug("Original Format: " + format);
String paramValue = getParameterValue(paramName);
format = replaceDefined(format, paramValue, vdef);
log.debug("Format after replaceDefined: " + format);
Pattern p = Pattern.compile("\\$\\w*");
Matcher m = p.matcher(format);
while (m.find())
{
if (m.group().contains("$value"))
{
format = format.replace("$value", paramValue);
} else if (m.group().contains("$vdef"))
{
format = format.replace("$vdef", vdef);
} else
{
String myKey = m.group().substring(1);
String myValue = null;
if (getParameterSet().contains(myKey) == false)
{
/*
TL: modified for raxmlhpc.xml "model" parameter. Need to get default value of parameters that
haven't been set in the gui but are referenced in this parameter's code element.
*/
myValue = piseMarshaller.getVdef(myKey);
if (myValue == null) // no vDef element in the pise xml
{
myValue = "";
}
// Terri - just added this when trying out the fix to precod restitution. Not sure if
// it breaks anything.
// sometimes users specify vdef empty string as "" instead of just leaving the vdef element empty.
// Todo: fix this in the piseMarshaller?
if (myValue.equals("\"\""))
{
myValue = "";
}
} else
{
myValue = getParameterValue(myKey);
}
format = format.replace(m.group(), myValue);
}
}
/*
A format can be a simple "..." or a condition (...)? "...":"..."
If it's ? expression we stick a "print" in front of both components.
But if it starts with "<<" we're going to assume the whole thing is a here document
and not process the ? operator. This isn't exactly what a real perl interpreter
would do but I think it's close enough for the pise and ngbw xml files we deal with these
days.
*/
if (!format.trim().startsWith("<<") && format.contains("?"))
{
format = format.substring(0, format.indexOf("?") + 1) + " print " + format.substring(format.indexOf("?") + 1);
format = format.substring(0, format.indexOf(":") + 1) + " print " + format.substring(format.indexOf(":") + 1);
} else
{
format = "print " + format;
}
format = format.replaceAll("false", "0");
format = format.replaceAll("true", "1");
log.debug("Restituted Format: " + format);
return format;
}
/*
Controls are written to express an error condition when true.
For example to require runtime to be <= 168, you write
"$runtime > 168.0"
Returns true (ie. all's well) if there are no controls or all controls evaluate to false.
For each control that is true, sets an error message in ??? TODO
*/
private boolean evaluateControls(List controls, String paramName, String vdef)
throws Exception
{
int errorCount = 0;
String perl;
String evaluatedPerl;
if (controls == null)
{
return true;
}
for (PiseMarshaller.Control c : controls)
{
perl = restitutionPrecond(c.perl, paramName, vdef);
evaluatedPerl= evaluatePerlStatement(perl);
//log.debug("ctrl: '" + perl + "' EVAL TO '" + evaluatedPerl + "'");
if (Boolean.valueOf(evaluatedPerl) == true)
{
parameterErrors.add(new FieldError(paramName, c.message));
errorCount += 1;
}
}
return errorCount == 0;
}
}
/*
NEWLINE replacement: (search for "NEWLINE" to see what this comment is referring to).
This code replaces the two character sequence of with a single newline character.
Since evalutePerl() returns the stdtout trimmed of leading and trailing whitespace, if we
want to have a parameter be followed by a newline, we need the result of the perl evaluation
to end not with a newline character but with a backslash followed by an n, which we'll replace
with a newline right here.
note that when you print a java string, as the logging messages in this file do, if
the string contains an actual newline, it will print on multiple lines. If you see
"\n" in the value displayed, it means the string contains two characters: a backslash
followed by an n. This is mostly what we see because if you put "\n" in an xml file
element, the xml unmarshaller delivers this as a java string containing a backslash
followed by an n.
Both perl (if using double quoted strings) and java interpret "\\n" as the two character
sequence . The first quote escapes the second, yielding a literal
backslash and the n is unquoted. In a perl single quoted string, '\\n' is three characters:
two backslashes followed by an n. If you have a format like '$value\\n', with single quotes,
perl leaves the \\n alone and when we get here we replace the \n with a newline, so we're
left with a single backslash followed by a newline ... probably not what you want.
On the other hand, "$value\\n" works because when ask perl to print this it gives us
the value followed by a backslash follwed by an n.
*/