/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.scripting;
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.WrappedException;
import org.mozilla.javascript.Wrapper;
import org.mozilla.javascript.debug.Debugger;
import org.mozilla.javascript.debug.IDebuggerWithWatchPoints;
import org.mozilla.javascript.xml.XMLObject;
import com.servoy.j2db.IServiceProvider;
import com.servoy.j2db.J2DBGlobals;
import com.servoy.j2db.Messages;
import com.servoy.j2db.dataprocessing.FoundSet;
import com.servoy.j2db.dataprocessing.IDataSet;
import com.servoy.j2db.dataprocessing.IModificationSubject;
import com.servoy.j2db.dataprocessing.IRecordInternal;
import com.servoy.j2db.dataprocessing.ModificationEvent;
import com.servoy.j2db.dataprocessing.ModificationSubject;
import com.servoy.j2db.persistence.Column;
import com.servoy.j2db.persistence.IColumnTypes;
import com.servoy.j2db.persistence.IScriptProvider;
import com.servoy.j2db.persistence.ISupportScriptProviders;
import com.servoy.j2db.persistence.ScriptVariable;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.IDelegate;
import com.servoy.j2db.util.ScopesUtils;
import com.servoy.j2db.util.UUID;
import com.servoy.j2db.util.Utils;
/**
* This scope holds the variables of the specific servoy elements (form/global).
* It remembers the type of the variable and will convert to that type when a value is set.
* Media types {@link IColumnTypes#MEDIA} will not be converted but will go into this scope as is.
*
* @author jblok
*/
public abstract class ScriptVariableScope extends LazyCompilationScope
{
private final Map<String, Integer> nameType; //name -> type
private Map<String, Integer> replacedNameType; //name -> type
public ScriptVariableScope(Scriptable parent, IExecutingEnviroment scriptEngine, ISupportScriptProviders scriptLookup)
{
super(parent, scriptEngine, scriptLookup);
nameType = new HashMap<String, Integer>();
nameType.put("_super", new Integer(IColumnTypes.MEDIA)); //$NON-NLS-1$
}
public void put(ScriptVariable var)
{
put(var, false);
}
/**
* @param variable
* @param b
*/
public void put(ScriptVariable var, boolean overwriteInitialValue)
{
putScriptVariable(var.getDataProviderID(), var, overwriteInitialValue);
}
protected void putScriptVariable(String name, ScriptVariable var, boolean overwriteInitialValue)
{
if (ScopesUtils.isVariableScope(name))
{
// global variable: name should have been stripped in GlobalScope
throw new RuntimeException("Trying to set variable '" + name + "' in non-global scope " + this);
}
int prevType = 0;
if (replacedNameType != null && replacedNameType.containsKey(name)) prevType = Utils.getAsInteger(replacedNameType.get(name));
else prevType = Utils.getAsInteger(nameType.get(name));
boolean existingWithSameType = (prevType == var.getVariableType());
if (!existingWithSameType || overwriteInitialValue)//if same as previous, then leave initial value, this happens in developer or from login->main
{
nameType.put(name, new Integer(var.getVariableType()));
if (replacedNameType != null) replacedNameType.remove(name);
Object initValue = var.getInitValue();
if (initValue instanceof String)
{
String str = (String)initValue;
if (str.trim().length() > 0)
{
int commentIndex = str.lastIndexOf("//"); //$NON-NLS-1$
if (commentIndex != -1)
{
int singleQuote = str.lastIndexOf('\'');
if (singleQuote < commentIndex)
{
int doubleQuote = str.lastIndexOf('"');
if (doubleQuote < commentIndex)
{
int nextNewLine = str.indexOf('\n', commentIndex);
if (nextNewLine == -1 || str.lastIndexOf('\n') <= nextNewLine)
{
str = str.substring(0, commentIndex).trim();
if (str.endsWith(";")) str = str.substring(0, str.length() - 1); //$NON-NLS-1$
}
}
}
}
str = '(' + str + ')'; // add brackets so that unnamed objects are evaluated correctly (otherwise it will give a syntax error)
}
String sourceName = var.getSerializableRuntimeProperty(IScriptProvider.FILENAME);
if (sourceName == null)
{
sourceName = name;
}
initValue = evalValue(name, str, sourceName, var.getLineNumberOffset());
}
putWithoutFireChange(name, initValue);
if (Utils.mapGetKeyByValue(allIndex, name) == null)
{//insert "inded-> name" only if "index -> name" isn't already present (case of form inheritance)
allIndex.put(new Integer(allIndex.size()), name);
}
}
}
/**
* @param name
* @param initValue
* @param str
* @return
*/
private Object evalValue(String name, String str, String sourceName, int lineNumber)
{
Object retValue = null;
Context cx = Context.enter();
boolean debug = cx.isGeneratingDebug();
int level = cx.getOptimizationLevel();
Debugger debugger = cx.getDebugger();
Object debuggerContextData = cx.getDebuggerContextData();
try
{
cx.setGeneratingDebug(lineNumber != -1);
if (lineNumber != -1 || Utils.getAsBoolean(System.getProperty(ScriptEngine.SERVOY_DISABLE_SCRIPT_COMPILE_PROPERTY, "false"))) //flag should only be used in rich client //$NON-NLS-1$
{
cx.setOptimizationLevel(-1);
}
else
{
cx.setOptimizationLevel(9);
}
//cx.setDebugger(null, null);
Object o = cx.evaluateString(this, str, sourceName, lineNumber == -1 ? 0 : lineNumber, null);
if (o instanceof Wrapper && !(o instanceof NativeArray))
{
o = ((Wrapper)o).unwrap();
}
if (o == Scriptable.NOT_FOUND || o == Undefined.instance)
{
o = null;
}
retValue = o;
Integer previousType = null;
if (retValue instanceof Date)
{
// this is really an instance of Date so the type should be reset to that.
previousType = nameType.put(name, new Integer(IColumnTypes.DATETIME));
}
else if (retValue instanceof XMLObject)
{
previousType = nameType.put(name, new Integer(Types.OTHER));
}
else if (retValue instanceof Number)
{
Integer prevType = nameType.get(name);
if (prevType == null || !(prevType.intValue() == IColumnTypes.NUMBER || prevType.intValue() == IColumnTypes.INTEGER))
{
previousType = nameType.put(name, new Integer(IColumnTypes.NUMBER));
}
}
else if (!(retValue instanceof String) && !(retValue == null && nameType.get(name) != null))
{
// this is not an instanceof a String and a Date so make it a
previousType = nameType.put(name, new Integer(IColumnTypes.MEDIA));
}
if (previousType != null)
{
if (replacedNameType == null)
{
replacedNameType = new HashMap<String, Integer>(3);
replacedNameType.put(name, previousType);
}
else if (!replacedNameType.containsKey(name))
{
replacedNameType.put(name, previousType);
}
}
}
catch (Exception ex)
{
IServiceProvider serviceProvider = J2DBGlobals.getServiceProvider();
if (serviceProvider != null)
{
serviceProvider.reportJSError("cant parse variable '" + name + '\'', ex);
}
else
{
Debug.log("cant parse variable '" + name + '\'', ex); //$NON-NLS-1$
}
}
finally
{
try
{
cx.setGeneratingDebug(debug);
cx.setOptimizationLevel(level);
cx.setDebugger(debugger, debuggerContextData);
}
finally
{
Context.exit();
}
}
return retValue;
}
public void putWithoutFireChange(String name, Object value)
{
Object oldVar = allVars.get(name);
if (value == null || !value.equals(oldVar))
{
allVars.put(name, value);
}
}
@Override
public Object get(int index, Scriptable start)
{
Object o = allIndex.get(new Integer(index));
if (o instanceof String && has((String)o, start))
{
return get((String)o, start);
}
return super.get(index, start);
}
@Override
public void put(int index, Scriptable start, Object value)
{
String name = (String)allIndex.get(new Integer(index));
if (name != null)
{
put(name, start, value);
}
else
{
super.put(index, start, value);
}
}
/**
* @see com.servoy.j2db.scripting.DefaultScope#getIds()
*/
@Override
public Object[] getIds()
{
// just return the names for a global scope
return allVars.keySet().toArray();
}
/*
* @see Scriptable#put(String, Scriptable, Object)
*/
@Override
public void put(String name, Scriptable arg1, Object value)
{
if (value instanceof Function)
{
super.put(name, arg1, value);
}
else
{
try
{
Context currentContext = Context.getCurrentContext();
if (currentContext != null)
{
Debugger debugger = currentContext.getDebugger();
if (debugger != null)
{
if (debugger instanceof IDebuggerWithWatchPoints)
{
IDebuggerWithWatchPoints wp = (IDebuggerWithWatchPoints)debugger;
wp.modification(name, this);
}
}
}
put(name, value);
}
catch (RuntimeException re)
{
throw new WrappedException(re);
}
}
}
public Object put(String name, Object val)
{
Object value = Utils.mapToNullIfUnmanageble(val);
Integer variableType = nameType.get(name);
int type = 0;
boolean xmlType = false;
if (variableType == null)
{
// global doesn't exist. dynamic new one.. so MEDIA type
type = IColumnTypes.MEDIA;
nameType.put(name, new Integer(type));
Debug.trace("Warning: " + name + " is not defined in the variables, a dynamic (media type) variable is created"); //$NON-NLS-1$//$NON-NLS-2$
}
else
{
if (variableType.intValue() == Types.OTHER)
{
type = IColumnTypes.MEDIA;
xmlType = true;
}
else
{
type = Column.mapToDefaultType(variableType.intValue());
}
}
if (type == IColumnTypes.TEXT)
{
Object txt = value;
while (txt instanceof IDelegate< ? >)
{
txt = ((IDelegate< ? >)txt).getDelegate();
}
if (txt instanceof IDataSet)
{
IDataSet set = (IDataSet)txt;
StringBuilder sb = new StringBuilder();
sb.append('\n');
for (int i = 0; i < set.getRowCount(); i++)
{
sb.append(set.getRow(i)[0]);
sb.append('\n');
}
value = sb.toString();
}
else if (txt instanceof FoundSet)
{
StringBuilder sb = new StringBuilder();
sb.append('\n');
FoundSet fs = (FoundSet)txt;
for (int i = 0; i < fs.getSize(); i++)
{
IRecordInternal record = fs.getRecord(i);
sb.append(record.getPKHashKey());
sb.append('\n');
}
value = sb.toString();
}
}
if (value != null && variableType != null)
{
Object unwrapped = value;
while (unwrapped instanceof Wrapper)
{
unwrapped = ((Wrapper)unwrapped).unwrap();
if (unwrapped == value)
{
break;
}
}
if (type == IColumnTypes.MEDIA)
{
if (!(unwrapped instanceof UUID))
{
Object previousValue = get(name);
if (previousValue instanceof UUID || previousValue == null)
{
Iterator<ScriptVariable> scriptVariablesIte = getScriptLookup().getScriptVariables(false);
ScriptVariable sv;
while (scriptVariablesIte.hasNext())
{
sv = scriptVariablesIte.next();
if (name.equals(sv.getName()))
{
if (UUID.class.getSimpleName().equals(getDeclaredType(sv)))
{
value = Utils.getAsUUID(unwrapped, false);
}
break;
}
}
}
}
}
else
{
value = unwrapped;
try
{
value = Column.getAsRightType(variableType.intValue(), Column.NORMAL_COLUMN, value, null, Integer.MAX_VALUE, null, true); // dont convert with timezone here, its not ui but from scripting
}
catch (Exception e)
{
throw new IllegalArgumentException(Messages.getString(
"servoy.conversion.error.global", new Object[] { name, Column.getDisplayTypeString(variableType.intValue()), value })); //$NON-NLS-1$
}
}
}
if (value instanceof Date)
{
// make copy so then when it is further used in js it won't change this one.
value = new Date(((Date)value).getTime());
}
else if (xmlType && value instanceof String)
{
value = evalValue(name, (String)value, "internal_anon", -1); //$NON-NLS-1$
}
Object oldVar = allVars.get(name);
allVars.put(name, value);
if (variableType != null && !Utils.equalObjects(oldVar, value))
{
fireModificationEvent(name, value);
}
return oldVar;
}
private String getDeclaredType(ScriptVariable sv)
{
String comment = sv.getComment();
int typeIdx;
if (comment != null && (typeIdx = comment.indexOf("@type")) != -1) //$NON-NLS-1$
{
int s = comment.indexOf('{', typeIdx + 5);
if (s != -1)
{
int e = comment.indexOf('}', s + 1);
if (e != -1)
{
return comment.substring(s + 1, e).trim();
}
}
}
return null;
}
/*
* _____________________________________________________________ JavaScriptModificationListener
*/
private final IModificationSubject modificationSubject = new ModificationSubject();
/**
* @return the modificationSubject
*/
public IModificationSubject getModificationSubject()
{
return modificationSubject;
}
private void fireModificationEvent(String name, Object value)
{
if (modificationSubject.hasListeners())
{
modificationSubject.fireModificationEvent(new ModificationEvent(getDataproviderEventName(name), unwrap(value), this));
}
}
protected String getDataproviderEventName(String name)
{
return name;
}
/**
* @param dataProviderID
* @return
*/
public Object get(String dataProviderID)
{
return unwrap(getImpl(dataProviderID, this));
}
public static Object unwrap(Object obj)
{
Object o = obj;
while (o instanceof Wrapper)
{
Object tmp = ((Wrapper)o).unwrap();
if (tmp == o) break;
if (tmp != null && (tmp instanceof Object[]))
{
Object[] array = (Object[])tmp;
Object[] newArray = new Object[array.length];
for (int i = 0; i < array.length; i++)
{
newArray[i] = unwrap(array[i]);
}
tmp = newArray;
}
o = tmp;
}
if (o instanceof XMLObject)
{
o = ((XMLObject)o).toString();
}
return o;
}
public void remove(ScriptVariable var)
{
if (locked) throw new WrappedException(new RuntimeException(Messages.getString("servoy.javascript.error.lockedForDeleteName", new Object[] { var }))); //$NON-NLS-1$
nameType.remove(var.getName());
if (replacedNameType != null) replacedNameType.remove(var.getName());
super.remove(var.getName());
}
@Override
public void destroy()
{
nameType.clear();
if (replacedNameType != null) replacedNameType.clear();
super.destroy();
}
}