/* * Copyright 2014 Effektif GmbH. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.effektif.script; import java.io.PrintWriter; import java.lang.reflect.Method; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.mozilla.javascript.Callable; import org.mozilla.javascript.Context; import org.mozilla.javascript.IdFunctionObject; import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.NativeObject; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.effektif.workflow.impl.data.DataTypeImpl; import com.effektif.workflow.impl.data.TypedValueImpl; import com.effektif.workflow.impl.data.types.ListTypeImpl; import com.effektif.workflow.impl.workflowinstance.ScopeInstanceImpl; import com.effektif.workflow.impl.workflowinstance.VariableInstanceImpl; /** * @author Tom Baeyens */ public class RhinoVariableScope implements Scriptable { private static final Logger log = LoggerFactory.getLogger(RhinoScriptService.class); protected ScopeInstanceImpl scopeInstance; protected Scriptable parentScope; public Map<String,Object> objects; protected Map<String,Callable> functions = null; protected Set<String> updated; protected Map<String,String> mappings; public RhinoVariableScope(ScopeInstanceImpl scopeInstance, Map<String,String> scriptToWorkflowMappings, PrintWriter console, Scriptable parentScope) { this.scopeInstance = scopeInstance; this.parentScope = parentScope; this.mappings = scriptToWorkflowMappings; this.updated = new HashSet<>(); initializeObjects(console); initializeFunctions(); } protected void initializeObjects(PrintWriter console) { this.objects = new HashMap<>(); this.objects.put("console", new Console(console)); this.objects.put("JSON", new JSON()); } protected void initializeFunctions() { functions = new HashMap<>(); functions.put("contains", new Callable() { @Override public Object call(Context context, Scriptable scope, Scriptable thisObject, Object[] args) { if (args==null || args.length!=2 || args[0]==null) { return false; } if (args[0] instanceof String) { return ((String)args[0]).contains((CharSequence) args[1]); } if (args[0] instanceof Collection) { return ((Collection)args[0]).contains(args[1]); } return false; } }); } @Override public Object get(String name, Scriptable start) { log.debug("get "+name+" | "+start); if (objects.containsKey(name)) { return objects.get(name); } if (functions.containsKey(name)) { return functions.get(name); } String variableId = mappings!=null ? mappings.get(name) : null; if (variableId==null) { variableId = name; } Object nativeValue = null; VariableInstanceImpl variableInstance = scopeInstance.findVariableInstance(variableId); if (variableInstance!=null) { TypedValueImpl typedValue = variableInstance.getTypedValue(); log.debug(" lazy loaded variable "+name+" = "+(typedValue!=null ? typedValue.value : "null")); nativeValue = convertInternalToNative(typedValue, name); } objects.put(name, nativeValue); return nativeValue; } @Override public boolean has(String name, Scriptable start) { log.debug("has "+name+" | "+start); if (objects.containsKey(name) || functions.containsKey(name)) { return true; } String variableId = mappings!=null ? mappings.get(name) : null; if (variableId==null) { variableId = name; } return scopeInstance.findVariableInstance(variableId)!=null; } @Override public void put(String name, Scriptable start, Object value) { log.debug("put "+name+" | "+start+" | "+value); objects.put(name, value); updated(name); } @Override public Scriptable getParentScope() { log.debug("getParentScope"); return parentScope; } @Override public Object[] getIds() { log.debug("getIds"); throw new RuntimeException("not supported"); } @Override public Scriptable getPrototype() { log.debug("getPrototype"); return parentScope.getPrototype(); } /** maps variableIds to internal values */ public Map<String,TypedValueImpl> getUpdatedVariableValues() { Map<String,TypedValueImpl> updatedValues = new HashMap<>(); for (String scriptVariableName: updated) { Object localObject = objects.get(scriptVariableName); String variableId = mappings!=null ? mappings.get(scriptVariableName) : null; if (variableId==null) { variableId = scriptVariableName; } VariableInstanceImpl variableInstance = scopeInstance.findVariableInstance(variableId); if (variableInstance!=null) { DataTypeImpl type = variableInstance.type; // NativeObject implements Map // NativeArray implements List // So the data type conversion from javascript to internal should work Object value = null; if (localObject instanceof DirtyCheckingNativeObject) { DirtyCheckingNativeObject nativeObject = (DirtyCheckingNativeObject) localObject; value = nativeObject.value; } else if (localObject instanceof DirtyCheckingNativeArray) { DirtyCheckingNativeArray nativeArray = (DirtyCheckingNativeArray) localObject; value = nativeArray.values; } else { value = localObject; } TypedValueImpl typedValue = new TypedValueImpl(type, value); updatedValues.put(variableId, typedValue); } } return updatedValues; } /** the dirty checking native objects will call this method when they are changed */ protected void updated(String name) { log.debug("updated: "+name); updated.add(name); } protected Object convertInternalToNative(TypedValueImpl typedValue, String name) { if (typedValue==null) { return null; } return convertInternalToNative(typedValue.type, typedValue.value, name); } protected Object convertInternalToNative(DataTypeImpl type, Object value, String name) { if (value==null) { return null; } Class< ? extends Object> valueClass = value.getClass(); if (String.class.isAssignableFrom(valueClass) || Number.class.isAssignableFrom(valueClass) || Boolean.class.isAssignableFrom(valueClass)) { return value; } if (type instanceof ListTypeImpl) { ListTypeImpl listType = (ListTypeImpl) type; return new DirtyCheckingNativeArray(listType.elementType, (List<Object>) value, name); } return new DirtyCheckingNativeObject(type, value, name); } public class DirtyCheckingNativeArray extends NativeArray { private static final long serialVersionUID = 1L; String name; DataTypeImpl elementType; List<Object> values; public DirtyCheckingNativeArray(DataTypeImpl elementType, List<Object> values, String name) { super(values.size()); this.name = name; this.values = values; } @Override public Object get(int index, Scriptable arg1) { log.debug(" get index "+index); Object elementValue = values.get(index); return convertInternalToNative(elementType, elementValue, name); } @Override public Object get(int index) { return get(index, null); } @Override public void add(int arg0, Object arg1) { updated(name); super.add(arg0, arg1); } @Override public boolean add(Object arg0) { updated(name); return super.add(arg0); } @Override public boolean addAll(Collection arg0) { updated(name); return super.addAll(arg0); } @Override public boolean addAll(int arg0, Collection arg1) { updated(name); return super.addAll(arg0, arg1); } @Override public void clear() { updated(name); super.clear(); } @Override public void defineOwnProperty(Context arg0, Object arg1, ScriptableObject arg2) { updated(name); super.defineOwnProperty(arg0, arg1, arg2); } @Override public void delete(int arg0) { updated(name); super.delete(arg0); } @Override public Object execIdCall(IdFunctionObject arg0, Context arg1, Scriptable arg2, Scriptable arg3, Object[] arg4) { updated(name); return super.execIdCall(arg0, arg1, arg2, arg3, arg4); } @Override public void put(int arg0, Scriptable arg1, Object arg2) { updated(name); super.put(arg0, arg1, arg2); } @Override public void put(String arg0, Scriptable arg1, Object arg2) { updated(name); super.put(arg0, arg1, arg2); } @Override public Object remove(int arg0) { updated(name); return super.remove(arg0); } @Override public boolean remove(Object arg0) { updated(name); return super.remove(arg0); } @Override public boolean removeAll(Collection arg0) { updated(name); return super.removeAll(arg0); } @Override public boolean retainAll(Collection arg0) { updated(name); return super.retainAll(arg0); } @Override public Object set(int arg0, Object arg1) { updated(name); return super.set(arg0, arg1); } @Override protected void setInstanceIdValue(int arg0, Object arg1) { updated(name); super.setInstanceIdValue(arg0, arg1); } } public class DirtyCheckingNativeObject extends NativeObject { private static final long serialVersionUID = 1L; String name; DataTypeImpl type; Object value; public DirtyCheckingNativeObject(DataTypeImpl type, Object value, String name) { this.name = name; this.type = type; this.value = value; } @Override public boolean has(String field, Scriptable arg1) { log.debug(" has field "+field); return true; } @Override public Object get(String field, Scriptable arg1) { log.debug(" get field "+field); TypedValueImpl typedFieldValue = type.dereference(value, field); return convertInternalToNative(typedFieldValue, field); } @Override public void clear() { updated(name); super.clear(); } @Override public Object put(Object arg0, Object arg1) { updated(name); return super.put(arg0, arg1); } @Override public void putAll(Map arg0) { updated(name); super.putAll(arg0); } @Override public Object remove(Object arg0) { updated(name); return super.remove(arg0); } @Override public void defineOwnProperty(Context arg0, Object arg1, ScriptableObject arg2) { updated(name); super.defineOwnProperty(arg0, arg1, arg2); } @Override public void put(String arg0, Scriptable arg1, Object arg2) { updated(name); super.put(arg0, arg1, arg2); } @Override public void setAttributes(String arg0, int arg1) { updated(name); super.setAttributes(arg0, arg1); } @Override public void defineConst(String arg0, Scriptable arg1) { updated(name); super.defineConst(arg0, arg1); } @Override public void defineFunctionProperties(String[] arg0, Class< ? > arg1, int arg2) { updated(name); super.defineFunctionProperties(arg0, arg1, arg2); } @Override public void defineOwnProperties(Context arg0, ScriptableObject arg1) { updated(name); super.defineOwnProperties(arg0, arg1); } @Override public void defineProperty(String arg0, Class< ? > arg1, int arg2) { updated(name); super.defineProperty(arg0, arg1, arg2); } @Override public void defineProperty(String arg0, Object arg1, int arg2) { updated(name); super.defineProperty(arg0, arg1, arg2); } @Override public void defineProperty(String arg0, Object arg1, Method arg2, Method arg3, int arg4) { updated(name); super.defineProperty(arg0, arg1, arg2, arg3, arg4); } @Override public void delete(int arg0) { updated(name); super.delete(arg0); } @Override public void put(int arg0, Scriptable arg1, Object arg2) { updated(name); super.put(arg0, arg1, arg2); } @Override public void putConst(String arg0, Scriptable arg1, Object arg2) { updated(name); super.putConst(arg0, arg1, arg2); } @Override public void setAttributes(int arg0, int arg1) { updated(name); super.setAttributes(arg0, arg1); } @SuppressWarnings("deprecation") @Override public void setAttributes(int arg0, Scriptable arg1, int arg2) { updated(name); super.setAttributes(arg0, arg1, arg2); } @Override public void setGetterOrSetter(String arg0, int arg1, Callable arg2, boolean arg3) { updated(name); super.setGetterOrSetter(arg0, arg1, arg2, arg3); } } @Override public Object get(int index, Scriptable start) { throw new RuntimeException("not supported"); } @Override public void put(int index, Scriptable start, Object value) { throw new RuntimeException("not supported"); } @Override public boolean has(int index, Scriptable start) { throw new RuntimeException("not supported"); } @Override public void delete(String name) { throw new RuntimeException("not supported"); } @Override public void delete(int index) { throw new RuntimeException("not supported"); } @Override public String getClassName() { throw new RuntimeException("not supported"); } @Override public Object getDefaultValue(Class< ? > hint) { throw new RuntimeException("not supported"); } @Override public boolean hasInstance(Scriptable instance) { throw new RuntimeException("not supported"); } @Override public void setParentScope(Scriptable arg0) { throw new RuntimeException("not supported"); } @Override public void setPrototype(Scriptable arg0) { throw new RuntimeException("not supported"); } }