package org.yamcs.algorithms;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.codehaus.janino.SimpleCompiler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yamcs.parameter.ParameterValue;
import org.yamcs.parameter.Value;
import org.yamcs.protobuf.Yamcs.Value.Type;
import org.yamcs.xtce.Algorithm;
import org.yamcs.xtce.BinaryParameterType;
import org.yamcs.xtce.BooleanDataEncoding;
import org.yamcs.xtce.BooleanParameterType;
import org.yamcs.xtce.DataEncoding;
import org.yamcs.xtce.EnumeratedParameterType;
import org.yamcs.xtce.FloatDataEncoding;
import org.yamcs.xtce.FloatParameterType;
import org.yamcs.xtce.InputParameter;
import org.yamcs.xtce.IntegerDataEncoding;
import org.yamcs.xtce.IntegerDataEncoding.Encoding;
import org.yamcs.xtce.IntegerParameterType;
import org.yamcs.xtce.OutputParameter;
import org.yamcs.xtce.Parameter;
import org.yamcs.xtce.ParameterType;
import org.yamcs.xtce.StringDataEncoding;
import org.yamcs.xtce.StringParameterType;
import org.yamcs.xtceproc.ParameterTypeProcessor;
import com.google.protobuf.ByteString;
/**
* Represents the execution context of one algorithm. An AlgorithmEngine is reused
* upon each update of one or more of its InputParameters.
* <p>
* This class will create and compile on-the-fly ValueBinding implementations for every
* unique combination of raw and eng types. The reason for this is to get the mapping
* correct from Java to JavaScript. Rhino (the default JavaScript engine for JDK ≤ 7)
* will map java Float, Integer, etc towards javascript Object, instead of Number. As
* a result, in javascript, using the plus operator on two supposed numbers would do a
* string concatenation instead of an addition.
* <p>
* Rather than changing the Rhino configuration (which would require drastic tampering
* of the maven-compiler-plugin in order to lift Sun's Access Restrictions on these
* internal classes), we generate classes with primitive raw/eng values when needed.
*/
public class ScriptAlgorithmExecutor extends AbstractAlgorithmExecutor {
static final Logger log = LoggerFactory.getLogger(ScriptAlgorithmExecutor.class);
static final String RETURN_VALUE_VAR_NAME="returnValue";
ScriptEngine scriptEngine; // Not shared with other algorithm engines
CompiledScript compiledScript;
// Keeps one ValueBinding instance per InputParameter, recycled on each algorithm run
private Map<InputParameter,ValueBinding> bindingsByInput = new HashMap<InputParameter,ValueBinding>();
// Keeps one OutputValueBinding instance per OutputParameter, recycled on each algorithm run
private Map<OutputParameter,OutputValueBinding> bindingsByOutput = new HashMap<OutputParameter,OutputValueBinding>();
// Each ValueBinding class represent a unique raw/eng type combination (== key)
private static Map<String, Class<ValueBinding>> valueBindingClasses = Collections.synchronizedMap(new HashMap<String,Class<ValueBinding>>());
ParameterTypeProcessor parameterTypeProcessor;
public ScriptAlgorithmExecutor(Algorithm algorithmDef, ScriptEngine scriptEngine, AlgorithmExecutionContext execCtx) {
super(algorithmDef, execCtx);
this.scriptEngine = scriptEngine;
this.parameterTypeProcessor = new ParameterTypeProcessor(execCtx.getProcessor().getProcessorData());
for(InputParameter inputParameter:algorithmDef.getInputSet()) {
// Default-define all input values to null to prevent ugly runtime errors
String scriptName = inputParameter.getInputName();
if(scriptName==null) {
scriptName = inputParameter.getParameterInstance().getParameter().getName();
}
scriptEngine.put(scriptName, null);
}
// Improve error msgs
scriptEngine.put(ScriptEngine.FILENAME, algorithmDef.getQualifiedName());
// Set empty output bindings so that algorithms can write their attributes
for(OutputParameter outputParameter:algorithmDef.getOutputSet()) {
OutputValueBinding valueBinding = new OutputValueBinding();
bindingsByOutput.put(outputParameter, valueBinding);
String scriptName = outputParameter.getOutputName();
if(scriptName==null) {
scriptName = outputParameter.getParameter().getName();
}
scriptEngine.put(scriptName, valueBinding);
}
if(scriptEngine instanceof Compilable) {
try {
compiledScript = ((Compilable)scriptEngine).compile(algorithmDef.getAlgorithmText());
} catch (ScriptException e) {
log.warn("Error while compiling algorithm {}: {}", algorithmDef.getName(), e.getMessage(), e);
}
}
}
@Override
protected void updateInput(InputParameter inputParameter, ParameterValue newValue) {
// First time for an inputParameter, it will register a ValueBinding object with the engine.
// Further calls will just update that object
if(!bindingsByInput.containsKey(inputParameter)) {
ValueBinding valueBinding = toValueBinding(newValue);
bindingsByInput.put(inputParameter, valueBinding);
for(InputParameter input:algorithmDef.getInputSet()) {
if(input.equals(inputParameter)) {
String scriptName=inputParameter.getInputName();
if(scriptName==null) {
scriptName=inputParameter.getParameterInstance().getParameter().getName();
}
scriptEngine.put(scriptName, valueBinding);
}
}
}
bindingsByInput.get(inputParameter).updateValue(newValue);
}
/* (non-Javadoc)
* @see org.yamcs.algorithms.AlgorithmExecutor#runAlgorithm(long, long)
*/
@Override
public synchronized List<ParameterValue> runAlgorithm(long acqTime, long genTime) {
log.trace("Running algorithm '{}'",algorithmDef.getName());
try {
if(compiledScript!=null) {
compiledScript.eval();
} else {
scriptEngine.eval(algorithmDef.getAlgorithmText());
}
} catch (ScriptException e) {
log.warn("Error while executing algorithm: "+e.getMessage(), e);
return Collections.emptyList();
}
Object returnValue = scriptEngine.get(RETURN_VALUE_VAR_NAME);
List<ParameterValue> outputValues=new ArrayList<ParameterValue>();
for(OutputParameter outputParameter:algorithmDef.getOutputSet()) {
String scriptName=outputParameter.getOutputName();
if(scriptName==null) {
scriptName=outputParameter.getParameter().getName();
}
if(scriptEngine.get(scriptName) instanceof OutputValueBinding) {
OutputValueBinding res = (OutputValueBinding) scriptEngine.get(scriptName);
if(res.updated && res.value != null) {
ParameterValue pv = convertScriptOutputToParameterValue(outputParameter.getParameter(), res);
pv.setAcquisitionTime(acqTime);
pv.setGenerationTime(genTime);
outputValues.add(pv);
}
} else {
log.warn("Error while executing algorithm {}. Wrong type of output parameter. Ensure you assign to '{}.value' and not directly to '{}'",
algorithmDef.getQualifiedName(), scriptName, scriptName);
}
}
propagateToListeners(returnValue, outputValues);
return outputValues;
}
private ParameterValue convertScriptOutputToParameterValue(Parameter parameter, OutputValueBinding binding) {
ParameterValue pval=new ParameterValue(parameter);
ParameterType ptype=parameter.getParameterType();
if(ptype instanceof EnumeratedParameterType) {
setRawValue(((EnumeratedParameterType) ptype).getEncoding(), pval, binding.value);
} else if(ptype instanceof IntegerParameterType) {
setRawValue(((IntegerParameterType) ptype).getEncoding(), pval, binding.value);
} else if(ptype instanceof FloatParameterType) {
setRawValue(((FloatParameterType) ptype).getEncoding(), pval, binding.value);
} else if(ptype instanceof BinaryParameterType) {
setRawValue(((BinaryParameterType) ptype).getEncoding(), pval, binding.value);
} else if(ptype instanceof StringParameterType) {
setRawValue(((StringParameterType) ptype).getEncoding(), pval, binding.value);
} else if(ptype instanceof BooleanParameterType) {
setRawValue(((BooleanParameterType) ptype).getEncoding(), pval, binding.value);
} else {
throw new IllegalArgumentException("Unsupported parameter type "+ptype);
}
parameterTypeProcessor.calibrate(pval);
return pval;
}
private void setRawValue(DataEncoding de, ParameterValue pval, Object outputValue) {
if(de instanceof IntegerDataEncoding) {
setRawIntegerValue((IntegerDataEncoding) de, pval, outputValue);
} else if(de instanceof FloatDataEncoding) {
setRawFloatValue((FloatDataEncoding) de, pval, outputValue);
} else if(de instanceof StringDataEncoding) {
pval.setRawValue(outputValue.toString());
} else if(de instanceof BooleanDataEncoding) {
if(outputValue instanceof Boolean) {
pval.setRawValue((Boolean)outputValue);
} else {
log.error("Could not set boolean value of parameter {}. Algorithm returned wrong type: {}", pval.getParameter().getName(), outputValue.getClass());
}
} else {
log.error("DataEncoding {} not implemented as a raw return type for algorithms", de);
throw new IllegalArgumentException("DataEncoding "+de+" not implemented as a raw return type for algorithms");
}
}
private void setRawIntegerValue(IntegerDataEncoding ide, ParameterValue pv, Object outputValue) {
long longValue;
if(outputValue instanceof Number) {
longValue=((Number)outputValue).longValue();
} else {
log.warn("Unexpected script return type for "+pv.getParameter().getName()+". Was expecting a number, but got: "+outputValue.getClass());
return; // TODO make exc, and catch to send to ev
}
if(ide.getSizeInBits() <= 32) {
if(ide.getEncoding() == Encoding.unsigned) {
pv.setRawUnsignedInteger((int)longValue);
} else {
pv.setRawSignedInteger((int)longValue);
}
} else {
if(ide.getEncoding() == Encoding.unsigned) {
pv.setRawUnsignedLong(longValue);
} else {
pv.setRawSignedLong(longValue);
}
}
}
private void setRawFloatValue(FloatDataEncoding fde, ParameterValue pv, Object outputValue) {
double doubleValue;
if(outputValue instanceof Number) {
doubleValue=((Number)outputValue).doubleValue();
} else {
log.warn("Unexpected script return type for {}. Was expecting a number, but got: {}", pv.getParameter().getName(), outputValue.getClass());
return; // TODO make exc, and catch to send to ev
}
if(fde.getSizeInBits() <= 32) {
pv.setRawValue((float) doubleValue);
} else {
pv.setRawValue(doubleValue);
}
}
@Override
public String toString() {
return "def.getName() " +scriptEngine;
}
private static ValueBinding toValueBinding(ParameterValue pval) {
try {
Class<ValueBinding> clazz=getOrCreateValueBindingClass(pval);
Constructor<ValueBinding> constructor=clazz.getConstructor();
return constructor.newInstance();
} catch (Exception e) {
throw new IllegalStateException("Could not instantiate object of custom class", e);
}
}
private static Class<ValueBinding> getOrCreateValueBindingClass(ParameterValue pval) {
String key;
if(pval.getRawValue()==null) {
key=""+pval.getEngValue().getType().getNumber();
} else {
key=pval.getRawValue().getType().getNumber()+"_"+pval.getEngValue().getType().getNumber();
}
if(valueBindingClasses.containsKey(key)) {
return valueBindingClasses.get(key);
} else {
String className="ValueBinding"+key;
StringBuilder source=new StringBuilder();
source.append("package org.yamcs.algorithms;\n");
source.append("import "+ByteString.class.getName()+";\n");
source.append("import "+ParameterValue.class.getName()+";\n")
.append("public class " + className + " extends ValueBinding {\n");
StringBuilder updateValueSource=new StringBuilder(" public void updateValue(ParameterValue v) {\n")
.append(" super.updateValue(v);\n");
if(pval.getRawValue() != null) {
updateValueSource.append(addValueType(source, pval.getRawValue(), true));
}
updateValueSource.append(addValueType(source, pval.getEngValue(), false));
updateValueSource.append(" }\n");
source.append(updateValueSource.toString());
source.append("}");
try {
SimpleCompiler compiler=new SimpleCompiler();
if(log.isTraceEnabled()) {
log.trace("Compiling this:\n"+source.toString());
}
compiler.cook(source.toString());
@SuppressWarnings("unchecked")
Class<ValueBinding> clazz=(Class<ValueBinding>) compiler.getClassLoader().loadClass("org.yamcs.algorithms."+className);
valueBindingClasses.put(key, clazz);
return clazz;
} catch(Exception e) {
throw new IllegalStateException("Could not compile custom class", e);
}
}
}
/**
* Appends a raw or eng field with a getter of the given value
* @return a matching code fragment to be included in the updateValue() method
*/
private static String addValueType(StringBuilder source, Value v, boolean raw) {
if(v.getType() == Type.BINARY) {
if(raw) {
source.append(" public ByteString rawValue;\n");
return " rawValue=v.getRawValue().getBinaryValue();\n";
} else {
source.append(" public ByteString value;\n");
return " value=v.getEngValue().getBinaryValue();\n";
}
} else if(v.getType() == Type.DOUBLE) {
if(raw) {
source.append(" public double rawValue;\n");
return " rawValue=v.getRawValue().getDoubleValue();\n";
} else {
source.append(" public double value;\n");
return " value=v.getEngValue().getDoubleValue();\n";
}
} else if(v.getType() == Type.FLOAT) {
if(raw) {
source.append(" public float rawValue;\n");
return " rawValue=v.getRawValue().getFloatValue();\n";
} else {
source.append(" public float value;\n");
return " value=v.getEngValue().getFloatValue();\n";
}
} else if(v.getType() == Type.UINT32) {
if(raw) {
source.append(" public int rawValue;\n");
return " rawValue=v.getRawValue().getUint32Value();\n";
} else {
source.append(" public int value;\n");
return " value=v.getEngValue().getUint32Value();\n";
}
} else if(v.getType() == Type.SINT32) {
if(raw) {
source.append(" public int rawValue;\n");
return " rawValue=v.getRawValue().getSint32Value();\n";
} else {
source.append(" public int value;\n");
return " value=v.getEngValue().getSint32Value();\n";
}
} else if(v.getType() == Type.UINT64) {
if(raw) {
source.append(" public long rawValue;\n");
return " rawValue=v.getRawValue().getUint64Value();\n";
} else {
source.append(" public long value;\n");
return " value=v.getEngValue().getUint64Value();\n";
}
} else if(v.getType() == Type.SINT64) {
if(raw) {
source.append(" public long rawValue;\n");
return " rawValue=v.getRawValue().getSint64Value();\n";
} else {
source.append(" public long value;\n");
return " value=v.getEngValue().getSint64Value();\n";
}
} else if(v.getType() == Type.STRING) {
if(raw) {
source.append(" public String rawValue;\n");
return " rawValue=v.getRawValue().getStringValue();\n";
} else {
source.append(" public String value;\n");
return " value=v.getEngValue().getStringValue();\n";
}
} else if(v.getType() == Type.BOOLEAN) {
if(raw) {
source.append(" public boolean rawValue;\n");
return " rawValue=v.getRawValue().getBooleanValue();\n";
} else {
source.append(" public boolean value;\n");
return " value=v.getEngValue().getBooleanValue();\n";
}
} else {
throw new IllegalArgumentException("Unexpected value of type "+v.getType());
}
}
}