/* * 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.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.obiba.magma.Value; import org.obiba.magma.ValueSequence; import org.obiba.magma.ValueType; import org.obiba.magma.js.MagmaJsEvaluationRuntimeException; import org.obiba.magma.js.ScriptableValue; import org.obiba.magma.lang.Booleans; import org.obiba.magma.type.BooleanType; import org.obiba.magma.type.DateTimeType; import org.obiba.magma.type.DateType; import org.obiba.magma.type.DecimalType; import org.obiba.magma.type.IntegerType; import org.obiba.magma.type.TextType; import com.google.common.base.Objects; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; /** * Methods of the {@code ScriptableValue} javascript class that deal with {@code ScriptableValue} of {@code BooleanType} * . Note that other methods that use {@code BooleanType} may be defined elsewhere. */ @SuppressWarnings( { "UnusedDeclaration", "ChainOfInstanceofChecks" }) public class BooleanMethods { private BooleanMethods() { } /** * <pre> * $('Categorical').any('CAT1', 'CAT2') * </pre> * * @return true when the value is equal to any of the parameter, false otherwise. Note that this method will always * return false if the value is null. */ public static ScriptableValue any(Context ctx, Scriptable thisObj, Object[] args, Function funObj) { ScriptableValue sv = (ScriptableValue) thisObj; if(sv.getValue().isNull()) { return buildValue(thisObj, false); } for(Object test : args) { Value testValue = sv.getValueType().valueOf(test); if(sv.contains(testValue)) { return buildValue(thisObj, true); } } return buildValue(thisObj, false); } /** * <pre> * $('Categorical').all('CAT1', 'CAT2') * </pre> * * @return true when the value contains all specified parameters, false otherwise. Note that this method will always * return false if the value is null. */ public static ScriptableValue all(Context ctx, Scriptable thisObj, Object[] args, @Nullable Function funObj) { ScriptableValue sv = (ScriptableValue) thisObj; if(sv.getValue().isNull()) { return buildValue(thisObj, false); } for(Object test : args) { Value testValue = null; if(test instanceof String) { testValue = sv.getValueType().valueOf(test); } else if(test instanceof ScriptableValue) { testValue = ((ScriptableValue) test).getValue(); } else { throw new MagmaJsEvaluationRuntimeException( "cannot invoke all() with argument of type " + test.getClass().getName()); } if(!sv.contains(testValue)) { return buildValue(thisObj, false); } } return buildValue(thisObj, true); } /** * Without arguments, must be applied to boolean values only. With arguments, should be considered as a 'not equals' * comparison test. * <p/> * <pre> * $('BooleanVar').not() * $('Categorical').any('CAT1').not() * $('Categorical').not('CAT1', 'CAT2') * $('Categorical').not($('Other Categorical')) * </pre> */ public static ScriptableValue not(Context ctx, Scriptable thisObj, Object[] args, Function funObj) { ScriptableValue sv = (ScriptableValue) thisObj; if(args != null && args.length > 0) { // Is of form .not(value) for(Object test : args) { Value testValue = sv.getValueType().valueOf(test); if(sv.getValue().isNull()) { if(testValue.isNull()) { return buildValue(thisObj, false); } } if(sv.contains(testValue)) { return buildValue(thisObj, false); } } return buildValue(thisObj, true); } // Is of form .not() return not(ctx, thisObj, funObj); } /** * <pre> * $('BooleanVar').and(someBooleanVar) * $('BooleanVar').and(firstBooleanVar, secondBooleanVar) * $('BooleanVar').and($('OtherBooleanVar')) * $('BooleanVar').and($('OtherBooleanVar').not()) * $('BooleanVar').and(someBooleanVar, $('OtherBooleanVar')) * </pre> */ @SuppressWarnings("PMD.NcssMethodCount") public static ScriptableValue and(Context ctx, Scriptable thisObj, @Nullable Object[] args, @Nullable Function funObj) { ScriptableValue sv = (ScriptableValue) thisObj; Value value = sv.getValue(); if(value.getValueType() != BooleanType.get()) { try { value = BooleanType.get().convert(value); } catch(IllegalArgumentException e) { throw new MagmaJsEvaluationRuntimeException( "cannot invoke and() for Value of type " + value.getValueType().getName()); } } Boolean booleanValue = toBoolean(value); if(args == null || args.length == 0) { return buildValue(thisObj, booleanValue); } for(Object arg : args) { if(arg instanceof ScriptableValue) { ScriptableValue operand = (ScriptableValue) arg; booleanValue = Booleans.ternaryAnd(booleanValue, toBoolean(operand.getValue())); } else { booleanValue = Booleans.ternaryAnd(booleanValue, ScriptRuntime.toBoolean(arg)); } if(Boolean.FALSE.equals(booleanValue)) { return buildValue(thisObj, false); } } return buildValue(thisObj, booleanValue); } /** * <pre> * $('BooleanVar').isNull() * </pre> */ public static ScriptableValue isNull(Context ctx, Scriptable thisObj, Object[] args, Function funObj) { ScriptableValue sv = (ScriptableValue) thisObj; return new ScriptableValue(thisObj, BooleanType.get().valueOf(sv.getValue().isNull())); } /** * <pre> * $('BooleanVar').isNotNull() * </pre> */ public static ScriptableValue isNotNull(Context ctx, Scriptable thisObj, Object[] args, Function funObj) { ScriptableValue sv = (ScriptableValue) thisObj; return new ScriptableValue(thisObj, BooleanType.get().valueOf(!sv.getValue().isNull())); } /** * Returns true {@code BooleanType} if the {@code ScriptableValue} .empty() is operating on is a sequence that * contains zero values. Otherwise false is returned. * <p/> * <pre> * $('Admin.Interview.exportLog.destination').empty() * </pre> */ public static ScriptableValue empty(Context ctx, Scriptable thisObj, @Nullable Object[] args, @Nullable Function funObj) { ScriptableValue sv = (ScriptableValue) thisObj; if(sv.getValue().isNull()) { return new ScriptableValue(thisObj, BooleanType.get().nullValue()); } if(sv.getValue().isSequence() && sv.getValue().asSequence().getSize() == 0) return new ScriptableValue(thisObj, BooleanType.get().trueValue()); return new ScriptableValue(thisObj, BooleanType.get().falseValue()); } /** * <pre> * $('BooleanVar').or(someBooleanVar) * $('BooleanVar').or($('OtherBooleanVar')) * $('BooleanVar').or($('OtherBooleanVar').not()) * </pre> */ public static ScriptableValue or(Context ctx, Scriptable thisObj, Object[] args, @Nullable Function funObj) { ScriptableValue sv = (ScriptableValue) thisObj; Value value = sv.getValue(); if(value.getValueType() != BooleanType.get()) { // try { value = BooleanType.get().convert(value); // } catch(IllegalArgumentException e) { // throw new MagmaJsEvaluationRuntimeException("cannot invoke or() for Value of type " + // value.getValueType().getName()); // } } Boolean booleanValue = toBoolean(value); if(args == null || args.length == 0) { return buildValue(thisObj, booleanValue); } for(Object arg : args) { if(arg instanceof ScriptableValue) { ScriptableValue operand = (ScriptableValue) arg; booleanValue = Booleans.ternaryOr(booleanValue, toBoolean(operand.getValue())); } else { booleanValue = Booleans.ternaryOr(booleanValue, ScriptRuntime.toBoolean(arg)); } if(Boolean.TRUE.equals(booleanValue)) { return buildValue(thisObj, true); } } return buildValue(thisObj, booleanValue); } /** * Returns a new {@link ScriptableValue} of the {@link BooleanType} indicating if the first parameter is equal to the * second parameter. * <p/> * <pre> * $('NumberVarOne').eq($('NumberVarTwo')) * $('BooleanVarOne').eq($('BooleanVarTwo')) * $('TextVarOne').eq($('TextVarTwo')) * </pre> * * @throws MagmaJsEvaluationRuntimeException if operands are not ScriptableValue Objects of a numeric type, * BooleanType or TextType. */ @SuppressWarnings({ "OverlyLongMethod", "PMD.NcssMethodCount" }) public static ScriptableValue eq(Context ctx, Scriptable thisObj, @Nullable Object[] args, @Nullable Function funObj) throws MagmaJsEvaluationRuntimeException { ScriptableValue firstOperand = (ScriptableValue) thisObj; if(args == null || args.length == 0) { return new ScriptableValue(thisObj, BooleanType.get().falseValue()); } // equivalent to isNull() if (args.length == 1 && args[0] == null) return new ScriptableValue(thisObj, BooleanType.get().valueOf(firstOperand.getValue().isNull())); List<Value> argValues = Lists.newArrayList(); for (Object arg : args) { argValues.add(arg instanceof ScriptableValue ? ((ScriptableValue) arg).getValue() : firstOperand.getValueType().valueOf(arg == null ? null : arg.toString())); } Value secondOperandValue = argValues.size() == 1 ? argValues.get(0) : firstOperand.getValueType().sequenceOf(argValues); return new ScriptableValue(thisObj, eqValue(firstOperand.getValue(), secondOperandValue)); } private static Value eqValueSequence(ValueSequence firstOperand, ValueSequence secondOperand) { if (firstOperand.getSize() != secondOperand.getSize()) return BooleanType.get().falseValue(); for (int i = 0; i<firstOperand.getSize(); i++) { Value eqAt = eqValue(firstOperand.get(i), secondOperand.get(i)); if (!(Boolean)eqAt.getValue()) return eqAt; } return BooleanType.get().trueValue(); } private static Value eqValue(Value firstOperand, Value secondOperand) { if (firstOperand.isNull() && secondOperand.isNull()) return BooleanType.get().trueValue(); if (firstOperand.isSequence()) { if (!secondOperand.isSequence()) return eqValueSequence(firstOperand.asSequence(), secondOperand.getValueType().sequenceOf(Lists.newArrayList(secondOperand))); return eqValueSequence(firstOperand.asSequence(), secondOperand.asSequence()); } if (secondOperand.isSequence()) return eqValueSequence(firstOperand.getValueType().sequenceOf(Lists.newArrayList(firstOperand)), secondOperand.asSequence()); if(firstOperand.getValueType().isNumeric() && secondOperand.getValueType().isNumeric()) { return numericEquals(firstOperand, secondOperand); } if (!firstOperand.getValueType().equals(secondOperand.getValueType())) return BooleanType.get().falseValue(); if(firstOperand.getValueType().equals(BooleanType.get()) && secondOperand.getValueType().equals(BooleanType.get())) { return booleanEquals(firstOperand, secondOperand); } if(firstOperand.getValueType().equals(TextType.get()) && secondOperand.getValueType().equals(TextType.get())) { return textEquals(firstOperand, secondOperand); } if(firstOperand.getValueType().equals(DateType.get()) && secondOperand.getValueType().equals(DateType.get())) { return dateEquals(firstOperand, secondOperand); } if(firstOperand.getValueType().equals(DateTimeType.get()) && secondOperand.getValueType().equals(DateTimeType.get())) { return dateTimeEquals(firstOperand, secondOperand); } throw new MagmaJsEvaluationRuntimeException( "Cannot invoke equals() with argument of type '" + firstOperand.getValueType().getName() + "' and '" + secondOperand.getValueType().getName() + "'."); } public static ScriptableValue whenNull(Context ctx, Scriptable thisObj, Object[] args, Function funObj) throws MagmaJsEvaluationRuntimeException { ScriptableValue sv = (ScriptableValue) thisObj; if(sv.getValue().isSequence()) { return whenNullSequence(ctx, thisObj, args, funObj); } if(sv.getValue().isNull() && args != null && args.length > 0) { return new ScriptableValue(thisObj, whenNullArgument(sv.getValueType(), args[0])); } return sv; } private static ScriptableValue whenNullSequence(Context ctx, Scriptable thisObj, Object[] args, Function funObj) throws MagmaJsEvaluationRuntimeException { ScriptableValue sv = (ScriptableValue) thisObj; if(sv.getValue().isNull()) { Value rval = whenNullArgument(sv.getValueType(), args[0]); return rval.isSequence() // ? new ScriptableValue(thisObj, rval) // : new ScriptableValue(thisObj, sv.getValueType().sequenceOf(Collections.singleton(rval))); } Collection<Value> newValues = new ArrayList<>(); for(Value val : sv.getValue().asSequence().getValues()) { if(val.isNull()) { newValues.add(whenNullArgument(val.getValueType(), args[0])); } else { newValues.add(val); } } return new ScriptableValue(thisObj, sv.getValueType().sequenceOf(newValues)); } private static Value whenNullArgument(ValueType type, Object arg) { return arg instanceof ScriptableValue ? ((ScriptableValue) arg).getValue() : type.valueOf(arg); } private static Value numericEquals(Value firstOperandValue, Value secondOperandValue) { if(firstOperandValue.isNull() || secondOperandValue.isNull()) { return BooleanType.get().valueOf(firstOperandValue.isNull() && secondOperandValue.isNull()); } Number firstNumber = (Number) firstOperandValue.getValue(); Number secondNumber = (Number) secondOperandValue.getValue(); if(firstOperandValue.getValueType().equals(IntegerType.get()) && secondOperandValue.getValueType().equals(IntegerType.get())) { return BooleanType.get().valueOf(Objects.equal(firstNumber, secondNumber)); } if(firstOperandValue.getValueType().equals(IntegerType.get()) && secondOperandValue.getValueType().equals(DecimalType.get())) { return BooleanType.get().valueOf(firstNumber.doubleValue() == (Double) secondNumber); } if(firstOperandValue.getValueType().equals(DecimalType.get()) && secondOperandValue.getValueType().equals(IntegerType.get())) { return BooleanType.get().valueOf((Double) firstNumber == secondNumber.doubleValue()); } return BooleanType.get().valueOf(Objects.equal(firstNumber, secondNumber)); } private static Value booleanEquals(Value firstOperandValue, Value secondOperandValue) { Boolean firstBoolean = firstOperandValue.isNull() ? Boolean.FALSE : (Boolean) firstOperandValue.getValue(); Boolean secondBoolean = secondOperandValue.isNull() ? Boolean.FALSE : (Boolean) secondOperandValue.getValue(); return BooleanType.get().valueOf(Objects.equal(firstBoolean, secondBoolean)); } private static Value textEquals(Value firstOperandValue, Value secondOperandValue) { String firstString = firstOperandValue.isNull() ? null : (String) firstOperandValue.getValue(); String secondString = secondOperandValue.isNull() ? null : (String) secondOperandValue.getValue(); return BooleanType.get().valueOf(Objects.equal(firstString, secondString)); } private static Value dateTimeEquals(Value firstOperandValue, Value secondOperandValue) { boolean result = firstOperandValue.equals(secondOperandValue); return BooleanType.get().valueOf(result); } private static Value dateEquals(Value firstOperandValue, Value secondOperandValue) { boolean result = firstOperandValue.equals(secondOperandValue); return BooleanType.get().valueOf(result); } private static ScriptableValue buildValue(Scriptable scope, @Nullable Boolean value) { return value == null ? new ScriptableValue(scope, BooleanType.get().nullValue()) : new ScriptableValue(scope, value ? BooleanType.get().trueValue() : BooleanType.get().falseValue()); } private static ScriptableValue not(Context ctx, Scriptable thisObj, Function funObj) { ScriptableValue sv = (ScriptableValue) thisObj; Value value = sv.getValue(); if(value.getValueType() == BooleanType.get()) { if(value.isNull()) { return new ScriptableValue(thisObj, BooleanType.get().nullValue()); } if(value.isSequence()) { // Transform the sequence of Boolean values to a sequence of !values Value notSeq = BooleanType.get().sequenceOf(Lists.newArrayList( Iterables.transform(value.asSequence().getValue(), new com.google.common.base.Function<Value, Value>() { @Override public Value apply(Value from) { // Transform the input into its invert boolean value return BooleanType.get().not(from); } }))); return new ScriptableValue(thisObj, notSeq); } return new ScriptableValue(thisObj, BooleanType.get().not(value)); } throw new MagmaJsEvaluationRuntimeException( "cannot invoke not() for Value of type " + value.getValueType().getName()); } @Nullable @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "Clients expect ternary methods to return null as a valid value.") private static Boolean toBoolean(Value value) { return value.isNull() ? null : (Boolean) value.getValue(); } }