/*
* 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.views;
import java.io.Serializable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextAction;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.obiba.magma.Initialisable;
import org.obiba.magma.Value;
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.js.MagmaContext;
import org.obiba.magma.js.ScriptableValue;
import org.obiba.magma.js.ScriptableVariable;
import org.obiba.magma.type.BooleanType;
import org.obiba.magma.views.SelectClause;
import org.obiba.magma.views.View;
import org.obiba.magma.views.WhereClause;
public class JavascriptClause implements Initialisable, SelectClause, WhereClause {
//
// Instance Variables
//
private String scriptName = "customScript";
private String script;
// need to be transient because of XML serialization
@SuppressWarnings("TransientFieldInNonSerializableClass")
private transient Script compiledScript;
//
// Constructors
//
/**
* No-arg constructor for XStream.
*/
public JavascriptClause() {
}
public JavascriptClause(String script) {
this.script = script;
}
//
// Initialisable Methods
//
@Override
public void initialise() throws EvaluatorException {
if(script == null) {
throw new NullPointerException("script cannot be null");
}
compiledScript = (Script) ContextFactory.getGlobal().call(new ContextAction() {
@Override
public Object run(Context cx) {
return cx.compileString(getScript(), getScriptName(), 1, null);
}
});
}
//
// SelectClause Methods
//
@Override
public boolean select(final Variable variable) {
if(compiledScript == null) {
throw new IllegalStateException("script hasn't been compiled. Call initialise() before calling select().");
}
if(variable == null) throw new IllegalArgumentException("variable cannot be null");
return (Boolean) ContextFactory.getGlobal().call(new ContextAction() {
@Override
@SuppressWarnings("ChainOfInstanceofChecks")
public Object run(Context ctx) {
MagmaContext context = MagmaContext.asMagmaContext(ctx);
// Don't pollute the global scope
Scriptable scope = new ScriptableVariable(context.newLocalScope(), variable);
Object value = compiledScript.exec(ctx, scope);
if(value instanceof Boolean) {
return value;
}
if(value instanceof ScriptableValue) {
ScriptableValue scriptable = (ScriptableValue) value;
if(scriptable.getValueType().equals(BooleanType.get())) {
return scriptable.getValue().isNull() ? null : scriptable.getValue().getValue();
}
}
return false;
}
});
}
//
// WhereClause Methods
//
@Override
public boolean where(final ValueSet valueSet) {
return where(valueSet, null);
}
@Override
public boolean where(final ValueSet valueSet, final View view) {
if(compiledScript == null) {
throw new IllegalStateException("script hasn't been compiled. Call initialise() before calling where().");
}
if(valueSet == null) throw new IllegalArgumentException("valueSet cannot be null");
return (Boolean) ContextFactory.getGlobal().call(new WhereContextAction(valueSet, view));
}
//
// Query Methods
//
@SuppressWarnings("UnusedDeclaration")
public Value query(final Variable variable) {
if(compiledScript == null) {
throw new IllegalStateException("script hasn't been compiled. Call initialise() before calling query().");
}
if(variable == null) throw new IllegalArgumentException("variable cannot be null");
return (Value) ContextFactory.getGlobal().call(new ContextAction() {
@SuppressWarnings("IfMayBeConditional")
@Override
public Object run(Context ctx) {
MagmaContext context = MagmaContext.asMagmaContext(ctx);
// Don't pollute the global scope
Scriptable scope = new ScriptableVariable(context.newLocalScope(), variable);
Object value = compiledScript.exec(ctx, scope);
if(value instanceof ScriptableValue) {
ScriptableValue scriptable = (ScriptableValue) value;
return scriptable.getValue();
} else if(value != null) {
return ValueType.Factory.newValue((Serializable) value);
} else {
// TODO: Determine what to return in case of null. Currently returning false (BooleanType).
return BooleanType.get().falseValue();
}
}
});
}
//
// Methods
//
public String getScriptName() {
return scriptName;
}
public void setScriptName(String name) {
scriptName = name;
}
public String getScript() {
return script;
}
public void setScript(String script) {
this.script = script;
}
/**
* This method is invoked before evaluating the script. It provides a chance for derived classes to initialise values
* within the context. This method will add the current {@code ValueSet} as a {@code ThreadLocal} variable with
* {@code ValueSet#class} as its key. This allows other classes to have access to the current {@code ValueSet} during
* the script's execution.
* <p/>
* Classes overriding this method must call their super class' method
*
* @param ctx the current context
* @param scope the scope of execution of this script
* @param valueSet the current {@code ValueSet}
*/
protected void enterContext(MagmaContext ctx, @SuppressWarnings("UnusedParameters") Scriptable scope,
ValueSet valueSet, View view) {
ctx.push(ValueSet.class, valueSet);
ctx.push(VariableEntity.class, valueSet.getVariableEntity());
ValueTable valueTable = valueSet.getValueTable();
ctx.push(ValueTable.class, valueTable);
if(view != null) {
ctx.push(View.class, view);
}
}
protected void exitContext(MagmaContext ctx, ValueSet valueSet, View view) {
ctx.pop(ValueSet.class);
ctx.pop(VariableEntity.class);
ctx.pop(ValueTable.class);
if(view != null) {
ctx.pop(View.class);
}
}
private class WhereContextAction implements ContextAction {
private final ValueSet valueSet;
private final View view;
WhereContextAction(ValueSet valueSet, View view) {
this.valueSet = valueSet;
this.view = view;
}
@Override
@SuppressWarnings("ChainOfInstanceofChecks")
public Object run(Context ctx) {
MagmaContext context = MagmaContext.asMagmaContext(ctx);
// Don't pollute the global scope
Scriptable scope = context.newLocalScope();
enterContext(context, scope, valueSet, view);
Object value = compiledScript.exec(ctx, scope);
exitContext(context, valueSet, view);
if(value instanceof Boolean) {
return value;
}
if(value instanceof ScriptableValue) {
return getValue((ScriptableValue) value);
}
return false;
}
private Object getValue(ScriptableValue scriptable) {
if (scriptable.getValue().isNull()) return false;
try {
return BooleanType.get().valueOf(scriptable.getValue().getValue()).getValue();
} catch (Exception e) {
return false;
}
}
}
}