/*
* Copyright (c) 2017 OBiBa. All rights reserved.
*
* This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.obiba.magma.js.methods;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import org.mozilla.javascript.ConsString;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.Scriptable;
import org.obiba.magma.NoSuchValueSetException;
import org.obiba.magma.Timestamps;
import org.obiba.magma.Value;
import org.obiba.magma.ValueSequence;
import org.obiba.magma.ValueSet;
import org.obiba.magma.ValueTable;
import org.obiba.magma.ValueType;
import org.obiba.magma.Variable;
import org.obiba.magma.VariableEntity;
import org.obiba.magma.VariableValueSource;
import org.obiba.magma.js.JavascriptValueSource.VectorCache;
import org.obiba.magma.js.MagmaContext;
import org.obiba.magma.js.MagmaJsEvaluationRuntimeException;
import org.obiba.magma.js.ScriptableValue;
import org.obiba.magma.js.ScriptableVariable;
import org.obiba.magma.support.MagmaEngineVariableResolver;
import org.obiba.magma.support.VariableEntityBean;
import org.obiba.magma.type.BooleanType;
import org.obiba.magma.type.DateTimeType;
import org.obiba.magma.type.TextType;
import org.obiba.magma.views.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@SuppressWarnings(
{ "IfMayBeConditional", "ChainOfInstanceofChecks", "OverlyCoupledClass", "StaticMethodOnlyUsedInOneClass" })
public final class GlobalMethods extends AbstractGlobalMethodProvider {
private static final Logger log = LoggerFactory.getLogger(GlobalMethods.class);
/**
* Set of methods to be exposed as top-level methods (ones that can be invoked anywhere)
*/
private static final Set<String> GLOBAL_METHODS = ImmutableSet
.of("$", "$val", "$value", "$created", "$lastupdate", "$this", "$join", "now", "log", "$var", "$variable", "$id",
"$identifier", "$group", "$groups", "newValue", "newSequence");
@Override
protected Set<String> getExposedMethods() {
return GLOBAL_METHODS;
}
/**
* Creates an instance of {@code ScriptableValue} containing the current date and time.
*
* @return an instance of {@code ScriptableValue} containing the current date and time.
*/
public static ScriptableValue now(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
return new ScriptableValue(thisObj, DateTimeType.get().valueOf(new Date()));
}
/**
* Creates a new value.
* <p/>
* <pre>
* newValue('Foo')
* newValue(123)
* newValue('123','integer')
* </pre>
*
* @return an instance of {@code ScriptableValue}
*/
public static ScriptableValue newValue(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
Object value = ensurePrimitiveValue(args[0]);
Value v = args.length > 1
? ValueType.Factory.forName((String) args[1]).valueOf(value)
: ValueType.Factory.newValue((Serializable) value);
return new ScriptableValue(thisObj, v);
}
/**
* Creates a new value sequence.
* <p/>
* <pre>
* newSequence('Foo')
* newSequence(['Foo', 'Bar'])
* newSequence(123)
* newSequence('123','integer')
* newSequence([123, 456])
* newSequence(['123', '456'],'integer')
* </pre>
*
* @return an instance of {@code ScriptableValue}
*/
public static ScriptableValue newSequence(Context cx, Scriptable thisObj, Object[] args, Function funObj) {
Object value = args[0];
ValueType valueType = args.length > 1 ? ValueType.Factory.forName((String) args[1]) : null;
List<Value> values;
if(value instanceof NativeArray) {
values = nativeArrayToValueList(valueType, (NativeArray) value);
} else {
values = new ArrayList<>();
value = ensurePrimitiveValue(value);
values.add(valueType == null ? ValueType.Factory.newValue((Serializable) value) : valueType.valueOf(value));
}
if(valueType == null) {
if(values.isEmpty()) {
throw new IllegalArgumentException("cannot determine ValueType for null object");
}
valueType = values.get(0).getValueType();
}
return new ScriptableValue(thisObj, ValueType.Factory.newSequence(valueType, values));
}
private static List<Value> nativeArrayToValueList(@Nullable ValueType valueType, NativeArray nativeArray) {
List<Value> newValues = new ArrayList<>();
ValueType vt = valueType;
if (vt == null) {
// try to guess the value type from the observed values
for(long i = 0; i < nativeArray.getLength(); i++) {
vt = inferValueType(nativeArray.get(i));
if (vt != null) break;
}
}
for(long i = 0; i < nativeArray.getLength(); i++) {
Object value = ensurePrimitiveValue(nativeArray.get(i));
Serializable serializable = (Serializable) value;
newValues.add(vt == null ? ValueType.Factory.newValue(serializable) : vt.valueOf(serializable));
}
return newValues;
}
private static Object ensurePrimitiveValue(Object value) {
Object rvalue = value;
if (value instanceof ScriptableValue) {
Value val = ((ScriptableValue) value).getValue();
rvalue = val.isNull() ? null : val.getValue();
} else if (value instanceof ConsString) {
rvalue = value.toString();
}
return rvalue;
}
private static ValueType inferValueType(Object value) {
if (value == null) return null;
if (value instanceof ScriptableValue)
return ((ScriptableValue) value).getValue().getValueType();
Object rvalue = value;
if (value instanceof ConsString) {
rvalue = value.toString();
}
return ValueType.Factory.newValue((Serializable) rvalue).getValueType();
}
/**
* Allows invoking {@code VariableValueSource#getValue(ValueSet)} and returns a {@code ScriptableValue}. Accessed as $
* in javascript.
* <p/>
* <pre>
* $('Participant.firstName')
* $('other-collection:SMOKER_STATUS')
* </pre>
*
* @return an instance of {@code ScriptableValue}
*/
public static Scriptable $(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
if(args.length != 1) {
throw new IllegalArgumentException("$() expects exactly one argument: a variable name.");
}
return $value(ctx, thisObj, args, funObj);
}
public static Scriptable $val(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
if(args.length != 1) {
throw new IllegalArgumentException("$val() expects exactly one argument: a variable name.");
}
return $value(ctx, thisObj, args, funObj);
}
public static Scriptable $value(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
if(args.length != 1) {
throw new IllegalArgumentException("$value() expects exactly one argument: a variable name.");
}
MagmaContext context = MagmaContext.asMagmaContext(ctx);
String name = (String) args[0];
return valueFromContext(context, thisObj, name);
}
/**
* Get the value set creation timestamp.
* <p/>
* <pre>
* $created()
* </pre>
*
* @param ctx
* @param thisObj
* @param args
* @param funObj
* @return
*/
public static Scriptable $created(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
MagmaContext context = MagmaContext.asMagmaContext(ctx);
return new ScriptableValue(thisObj, timestampsFromContext(context).getCreated());
}
/**
* Get the value set last update timestamp.
* <p/>
* <pre>
* $lastupdate()
* </pre>
*
* @param ctx
* @param thisObj
* @param args
* @param funObj
* @return
*/
public static Scriptable $lastupdate(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
MagmaContext context = MagmaContext.asMagmaContext(ctx);
return new ScriptableValue(thisObj, timestampsFromContext(context).getLastUpdate());
}
/**
* Allows invoking {@code VariableValueSource#getValue(ValueSet)} and returns a {@code ScriptableValue}.
* Accessed as $this in javascript. Argument is expected to be the name of a variable from the current view.
* <p/>
* <pre>
* $this('SMOKER_STATUS')
* </pre>
*
* @return an instance of {@code ScriptableValue}
*/
public static Scriptable $this(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
if(args.length != 1) {
throw new IllegalArgumentException("$this() expects exactly one argument: a variable name.");
}
MagmaContext context = MagmaContext.asMagmaContext(ctx);
if(!context.has(View.class)) {
throw new IllegalArgumentException("$this() can only be used in the context of a view.");
}
String name = (String) args[0];
if(name.contains(":")) {
throw new IllegalArgumentException("$this() expects a variable name of the current view.");
}
return valueFromViewContext(context, thisObj, name);
}
/**
* Allows joining a variable value to another variable value that provides a entity identifier. Accessed as $join in
* javascript. The treament of value sequences of value sequences is optional:
* <ul>
* <li>keep the occurrence order and therefore the values of a sequence are turned into a csv string,</li>
* <li>flatten the value sequence tree into a squence of unique values.</li>
* </ul>
* <p/>
* <pre>
* $join('medications.Drugs:BRAND_NAME','MEDICATION_1')
* $join('test.tbl:SEQ','VARSEQ', true)
* </pre>
*
* @return an instance of {@code ScriptableValue}
*/
public static Scriptable $join(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
if(args.length < 2) {
throw new IllegalArgumentException(
"$join() expects exactly two arguments: the reference the variable to be joined and the name of the variable holding entity identifiers.");
}
MagmaContext context = MagmaContext.asMagmaContext(ctx);
String joinedName = (String) args[0];
String name = (String) args[1];
boolean flat = false;
if (args.length == 3) {
try {
flat = (Boolean) BooleanType.get().valueOf(args[2]).getValue();
} catch (Exception ignore) {}
}
ValueTable valueTable = context.peek(ValueTable.class);
Value identifier = valueFromContext(context, thisObj, name).getValue();
// Find the joined named source
MagmaEngineVariableResolver reference = MagmaEngineVariableResolver.valueOf(joinedName);
ValueTable joinedTable = reference.resolveTable(valueTable);
VariableValueSource joinedSource = reference.resolveSource(valueTable);
return new ScriptableValue(thisObj, getJoinedValue(joinedTable, joinedSource, identifier, flat),
joinedSource.getVariable().getUnit());
}
/**
* Allows accessing the variable with the given name.
* <p/>
* <pre>
* $var('DO_YOU_SMOKE')
* </pre>
*
* @return an instance of {@code ScriptableVariable}
*/
public static Scriptable $var(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
return $variable(ctx, thisObj, args, funObj);
}
public static Scriptable $variable(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
if(args.length != 1) {
throw new IllegalArgumentException("$var() expects exactly one argument: a variable name.");
}
MagmaContext context = MagmaContext.asMagmaContext(ctx);
String name = (String) args[0];
return new ScriptableVariable(thisObj, variableFromContext(context, name));
}
/**
* Allows accessing the current entity identifier.
* <p/>
* <pre>
* $id()
* </pre>
*
* @return an instance of {@code ScriptableValue}
*/
public static Scriptable $id(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
return $identifier(ctx, thisObj, args, funObj);
}
public static Scriptable $identifier(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
MagmaContext context = MagmaContext.asMagmaContext(ctx);
VariableEntity entity = context.peek(VariableEntity.class);
return new ScriptableValue(thisObj, TextType.get().valueOf(entity.getIdentifier()));
}
/**
* Provides 'info' level logging of messages and variables. Returns a {@code ScriptableValue}. Accessed as 'log' in
* javascript.
* <p/>
* <pre>
* log('My message')
* log(onyx('org.obiba.onyx.lastExportDate'))
* log('The last export date: {}', onyx('org.obiba.onyx.lastExportDate'))
* log('The last export date: {} Days before purge: {}', onyx('org.obiba.onyx.lastExportDate'), onyx('org.obiba.onyx.participant.purge'))
* </pre>
*
* @return an instance of {@code ScriptableValue}
*/
public static Scriptable log(Context ctx, Scriptable thisObj, Object[] args, Function funObj) {
if(args.length < 1) {
throw new UnsupportedOperationException(
"log() expects either one or more arguments. e.g. log('message'), log('var 1 {}', $('var1')), log('var 1 {} var 2 {}', $('var1'), $('var2')).");
}
if(args.length == 1) {
if(args[0] instanceof Exception) {
log.warn("Exception during JS execution", (Throwable) args[0]);
} else {
log.info(args[0].toString());
}
} else {
log.info(args[0].toString(), Arrays.copyOfRange(args, 1, args.length));
}
return thisObj;
}
//
// Private methods
//
/**
* Get a joined value where identifier can be a sequence of identifiers.
*
* @param joinedTable
* @param joinedSource
* @param identifier
* @param flat Flatten the value sequence tree into a sequence of unique values
* @return
*/
private static Value getJoinedValue(ValueTable joinedTable, VariableValueSource joinedSource, Value identifier, boolean flat) {
// Default value is null if joined table has no valueSet (equivalent to a LEFT JOIN)
Value value = identifier.isSequence()
? joinedSource.getValueType().nullSequence()
: joinedSource.getValueType().nullValue();
if(identifier.isSequence()) {
if(identifier.asSequence().getSize() > 0) {
List<Value> joinedValues = Lists.newArrayList();
for(Value id : identifier.asSequence().getValue()) {
joinedValues.add(getSingleJoinedValue(joinedTable, joinedSource, id, flat));
}
value = joinedSource.getValueType().sequenceOf(joinedValues);
if (flat) {
value = value.getValueType().sequenceOf(new HashSet<>(getAllSingleValues(value.asSequence())));
}
}
} else {
value = getSingleJoinedValue(joinedTable, joinedSource, identifier, true);
}
return value;
}
/**
* Get a joined value where identifier must not be a sequence of identifiers.
*
* @param joinedTable
* @param joinedSource
* @param identifier
* @param allowSequence
* @return
*/
private static Value getSingleJoinedValue(ValueTable joinedTable, VariableValueSource joinedSource,
Value identifier, boolean allowSequence) {
Value value = identifier.isSequence()
? joinedSource.getValueType().nullSequence()
: joinedSource.getValueType().nullValue();
if(!identifier.isNull()) {
VariableEntity entity = new VariableEntityBean(joinedTable.getEntityType(), identifier.toString());
if(joinedTable.hasValueSet(entity)) {
value = joinedSource.getValue(joinedTable.getValueSet(entity));
value = allowSequence ? value : ensureValueNotSequence(value);
}
}
return value;
}
/**
* Make the value flat, in order to not have sequence of values that are value sequences.
*
* @param value
* @return
*/
private static Value ensureValueNotSequence(Value value) {
Value rval = value;
if(value.isSequence() && !value.asSequence().isNull()) {
if(value.asSequence().getSize() > 1) {
rval = TextType.get().valueOf(value.asSequence());
} else {
rval = TextType.get().valueOf(value.asSequence().get(0));
}
}
return rval;
}
/**
* Recursively gets all the values in the given sequence
* @param seq
* @return list of values
*/
public static List<Value> getAllSingleValues(ValueSequence seq) {
List<Value> list = new ArrayList<>();
extractSingleValues(seq, list);
return list;
}
private static void extractSingleValues(ValueSequence seq, List<Value> toAdd) {
for (Value v: seq.getValues()) {
if (v.isSequence()) {
extractSingleValues((ValueSequence)v, toAdd);
} else {
toAdd.add(v);
}
}
}
private static Scriptable valueFromViewContext(MagmaContext context, Scriptable thisObj, String name) {
View view = context.peek(View.class);
MagmaEngineVariableResolver reference = MagmaEngineVariableResolver.valueOf(name);
// Find the named source, which is in this context a view variable value source.
VariableValueSource source = reference.resolveSource(view);
// Test whether this is a vector-oriented evaluation or a ValueSet-oriented evaluation
if(context.has(VectorCache.class)) {
return valuesForVector(context, thisObj, reference, source);
}
ValueSet valueSet = context.peek(ValueSet.class);
// The ValueSet is the one of the "from" table of the view
ValueSet viewValueSet = view.getValueSetMappingFunction().apply(valueSet);
Value value = source.getValue(viewValueSet);
return new ScriptableValue(thisObj, value, source.getVariable().getUnit());
}
private static Timestamps timestampsFromContext(MagmaContext context) {
// Test whether this is a vector-oriented evaluation or a ValueSet-oriented evaluation
if(context.has(VectorCache.class)) {
ValueTable valueTable = context.peek(ValueTable.class);
VectorCache cache = context.peek(VectorCache.class);
return cache.get(context, valueTable);
} else {
ValueSet valueSet = context.peek(ValueSet.class);
return valueSet.getTimestamps();
}
}
private static ScriptableValue valueFromContext(MagmaContext context, Scriptable thisObj, String name) {
ValueTable valueTable = context.peek(ValueTable.class);
MagmaEngineVariableResolver reference = MagmaEngineVariableResolver.valueOf(name);
VariableValueSource variableSource = reference.resolveSource(valueTable);
// Test whether this is a vector-oriented evaluation or a ValueSet-oriented evaluation
return context.has(VectorCache.class)
? valuesForVector(context, thisObj, reference, variableSource)
: valueForValueSet(context, thisObj, reference, variableSource);
}
private static ScriptableValue valuesForVector(MagmaContext context, Scriptable thisObj,
MagmaEngineVariableResolver reference, VariableValueSource variableSource) {
// Load the vector
VectorCache cache = context.peek(VectorCache.class);
return valueForReference(context, thisObj, reference, variableSource, cache.get(context, variableSource.asVectorSource()));
}
private static ScriptableValue valueForValueSet(MagmaContext context, Scriptable thisObj,
MagmaEngineVariableResolver reference, VariableValueSource variableSource) {
ValueSet valueSet = context.peek(ValueSet.class);
// Tests whether this valueSet is in the same table as the referenced ValueTable
if(reference.isJoin(valueSet)) {
// Resolve the joined valueSet
try {
valueSet = reference.join(valueSet);
} catch(NoSuchValueSetException e) {
// Entity does not have a ValueSet in joined collection
// Return a null value
return new ScriptableValue(thisObj, variableSource.getValueType().nullValue(),
variableSource.getVariable().getUnit());
}
}
return valueForReference(context, thisObj, reference, variableSource, variableSource.getValue(valueSet));
}
private static ScriptableValue valueForReference(MagmaContext context, Scriptable thisObj,
MagmaEngineVariableResolver reference, VariableValueSource variableSource, Value value) {
// OPAL-2876 we want the value from a specific table
if(value.isSequence() && variableSource instanceof JoinVariableValueSource && ((JoinVariableValueSource)variableSource).isMultiple()
&& reference.hasDatasourceName() && reference.hasTableName()) {
int pos = ((JoinVariableValueSource)variableSource).getValueTablePosition(reference.getDatasourceName(), reference.getTableName());
return ValueSequenceMethods.valueAt(context, new ScriptableValue(thisObj, value), new Object[] {pos}, null);
}
return new ScriptableValue(thisObj, value, variableSource.getVariable().getUnit());
}
/**
* Get occurrence group matching criteria and returns a map (variable name/{@code ScriptableValue}).
* <p/>
* <pre>
* $group('StageName','StageA')['StageDuration']
* $group('NumVar', function(value) {
* return value.ge(10);
* })['AnotherVar']
* $group('StageName','StageA', 'StageDuration')
* </pre>
*
* @return a javascript object that maps variable names to {@code ScriptableValue}
*/
public static Object $group(Context ctx, Scriptable thisObj, Object[] args, Function funObj)
throws MagmaJsEvaluationRuntimeException {
if(args.length < 2 || args.length > 3) {
throw new IllegalArgumentException(
"$group() expects two required arguments (a variable name and a matching criteria (i.e. a value or a function)) " +
"and one optional argument (a variable name from the same occurrence group).");
}
// name of the 'source' variable on which the criteria is to be applied
String name = (String) args[0];
// criteria for selecting values of the 'source' variable
Object criteria = args[1];
// 'destination' variable to be selected
String select = args.length == 3 ? (String) args[2] : null;
if(args.length == 2) {
return getGroups(ctx, thisObj, name, criteria);
}
return new ScriptableValue(thisObj, getGroupValue(ctx, thisObj, name, criteria, select));
}
private static Value getGroupValue(Context ctx, Scriptable thisObj, String name, Object criteria, String select) {
MagmaContext context = MagmaContext.asMagmaContext(ctx);
ScriptableValue sv = valueFromContext(context, thisObj, name);
Variable variable = variableFromContext(context, name);
ValueTable valueTable = valueTableFromContext(context);
Variable selectVariable = getVariableFromOccurrenceGroup(valueTable, variable, select);
ValueSequence sourceValue = sv.getValue().asSequence();
if(sourceValue.isNull() || !sourceValue.isSequence()) {
return selectVariable.getValueType().nullValue();
}
Predicate<Value> predicate = getPredicate(ctx, sv.getParentScope(), thisObj, variable, criteria);
ValueSequence destinationValue = valueFromContext(context, thisObj, selectVariable.getName()).getValue()
.asSequence();
return getSequenceGroupValue(selectVariable.getValueType(), sourceValue, predicate, destinationValue);
}
private static Value getSequenceGroupValue(ValueType valueType, ValueSequence sourceValue, Predicate<Value> predicate,
ValueSequence destinationValue) {
List<Value> rvalues = Lists.newArrayList();
int index = -1;
for(Value value : sourceValue.getValues()) {
index++;
if(predicate.apply(value) && index < destinationValue.getSize()) {
rvalues.add(destinationValue.get(index));
}
}
if(rvalues.size() == 1) {
return rvalues.get(0);
}
if(rvalues.size() > 1) {
return valueType.sequenceOf(rvalues);
}
return valueType.nullValue();
}
@Deprecated
@SuppressWarnings({ "OverlyLongMethod", "PMD.NcssMethodCount" })
private static NativeObject getGroups(Context ctx, Scriptable thisObj, String name, Object criteria) {
MagmaContext context = MagmaContext.asMagmaContext(ctx);
ScriptableValue sv = valueFromContext(context, thisObj, name);
Variable variable = variableFromContext(context, name);
NativeObject valueObject = new NativeObject();
if(sv.getValue().isNull() || !sv.getValue().isSequence()) {
// just map itself
valueObject.put(variable.getName(), valueObject, sv);
} else {
ValueTable valueTable = valueTableFromContext(context);
Predicate<Value> predicate = getPredicate(ctx, sv.getParentScope(), thisObj, variable, criteria);
Iterable<Variable> variables = getVariablesFromOccurrenceGroup(valueTable, variable, null);
ValueSequence valueSequence = sv.getValue().asSequence();
Map<String, List<Value>> valueMap = Maps.newHashMap();
int index = -1;
// foreach eligible value, look for corresponding values of the same variable group
for(Value value : valueSequence.getValue()) {
index++;
if(predicate.apply(value)) {
// map itself
addVariableValue(valueMap, variable, value);
// get variables of the same occurrence group and map values
mapValues(context, thisObj, valueMap, variables, index);
}
}
// make it a native map
for(Map.Entry<String, List<Value>> entry : valueMap.entrySet()) {
List<Value> values = entry.getValue();
Value value;
if(values.size() == 1) {
value = values.get(0);
} else {
value = values.get(0).getValueType().sequenceOf(values);
}
valueObject.put(entry.getKey(), valueObject, new ScriptableValue(thisObj, value));
}
}
return valueObject;
}
@Nullable
private static ValueTable valueTableFromContext(MagmaContext context) {
ValueTable valueTable = null;
if(context.has(ValueTable.class)) {
valueTable = context.peek(ValueTable.class);
}
return valueTable;
}
private static Variable variableFromContext(MagmaContext context, String name) {
MagmaEngineVariableResolver reference = MagmaEngineVariableResolver.valueOf(name);
VariableValueSource source = context.has(ValueTable.class)
? reference.resolveSource(context.peek(ValueTable.class))
: reference.resolveSource();
return source.getVariable();
}
private static Predicate<Value> getPredicate(Context ctx, Scriptable scope, Scriptable thisObj, Variable variable,
Object criteria) {
Predicate<Value> predicate;
if(criteria instanceof ScriptableValue) {
predicate = new ValuePredicate(((ScriptableValue) criteria).getValue());
} else if(criteria instanceof Function) {
predicate = new FunctionPredicate(ctx, scope, thisObj, (Function) criteria);
} else {
predicate = new ValuePredicate(variable.getValueType().valueOf(criteria));
}
return predicate;
}
private static Variable getVariableFromOccurrenceGroup(@Nullable ValueTable valueTable, @NotNull Variable variable,
@NotNull String select) {
List<Variable> variables = getVariablesFromOccurrenceGroup(valueTable, variable, select);
if(variables.size() != 1) {
throw new IllegalArgumentException(
"Cannot find one variable with name '" + select + "' in the same occurrence group as '" + variable.getName() +
"'");
}
return variables.get(0);
}
private static List<Variable> getVariablesFromOccurrenceGroup(@Nullable ValueTable valueTable,
@NotNull Variable variable, String select) {
ImmutableList.Builder<Variable> builder = ImmutableList.builder();
if(variable.getOccurrenceGroup() == null || valueTable == null) {
return builder.build();
}
for(Variable var : valueTable.getVariables()) {
if(variable.getOccurrenceGroup().equals(var.getOccurrenceGroup())) {
if(select == null || select.equals(var.getName())) {
builder.add(var);
}
}
}
return builder.build();
}
private static void addVariableValue(Map<String, List<Value>> valueMap, Variable variable, Value value) {
List<Value> values = valueMap.get(variable.getName());
if(values == null) {
values = Lists.newArrayList();
valueMap.put(variable.getName(), values);
}
values.add(value);
}
private static void mapValues(MagmaContext context, Scriptable thisObj, Map<String, List<Value>> valueMap,
Iterable<Variable> variables, int index) {
if(index < 0) return;
for(Variable var : variables) {
ScriptableValue scriptableValue = valueFromContext(context, thisObj, var.getName());
Value value = var.getValueType().nullValue();
if(!scriptableValue.getValue().isNull()) {
ValueSequence valSeq = scriptableValue.getValue().asSequence();
if(index < valSeq.getSize()) {
value = valSeq.get(index);
}
}
addVariableValue(valueMap, var, value);
}
}
/**
* Predicate based on a function call.
*/
private static final class FunctionPredicate implements Predicate<Value> {
private final Context ctx;
private final Scriptable scope;
private final Scriptable thisObj;
private final Function criteriaFunction;
private FunctionPredicate(Context ctx, Scriptable scope, Scriptable thisObj, Function criteriaFunction) {
this.ctx = ctx;
this.scope = scope;
this.thisObj = thisObj;
this.criteriaFunction = criteriaFunction;
}
@Override
public boolean apply(@Nullable Value input) {
if(input == null) return false;
Object rval = criteriaFunction
.call(ctx, scope, thisObj, new ScriptableValue[] { new ScriptableValue(thisObj, input) });
if(rval instanceof ScriptableValue) {
Value value = ((ScriptableValue) rval).getValue();
if(value.isNull()) return false;
rval = value.getValue();
}
return rval == null ? false : (Boolean) rval;
}
}
/**
* Predicate based on the equality with a value.
*/
private static final class ValuePredicate implements Predicate<Value> {
@NotNull
private final Value criteriaValue;
private ValuePredicate(@NotNull Value criteriaValue) {
this.criteriaValue = criteriaValue;
}
@Override
public boolean apply(@Nullable Value input) {
return Objects.equal(input, criteriaValue);
}
}
}