blob: 8565ab9907d7501a992114c31933cbc43260e6f9 [file] [log] [blame]
/* --COPYRIGHT--,EPL
* Copyright (c) 2008 Texas Instruments and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Texas Instruments - initial implementation
*
* --/COPYRIGHT--*/
package xdc.services.getset;
import java.util.HashSet;
import java.util.Set;
import org.mozilla.javascript.BaseFunction;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.JavaScriptException;
import xdc.services.intern.xsr.Value;
import xdc.services.intern.xsr.Member;
import xdc.services.intern.xsr.XScriptO;
import xdc.services.intern.xsr.Extern;
/**
* A setter function to be added to a field of an XDCscript
* Value.Obj object. The Setters service manages a set of user-defined
* setter functions that notifies client code of a change to the value of
* the field. The setter only fires on writes to the field that
* actually change its value. Writing the current value again doesn't fire
* the setter.<p/>
*
* There is also a set of global user-defined setter functions that are
* notified on any change. This avoids having to manually add such setters
* to every field. Note, though, that the global setters only apply to
* fields that have had the Setters service added to them.<p/>
*
* Exceptions thrown by a setter are handled as follows:
* - the original value of the config is restored (without triggering
* any setters)
* - any setters for the config called before the exception are called
* again with the old and new values reversed and an additional
* exception object (passed as a forth argument)
* - calls to any setters that had not yet been called are never
* executed
* The JavaScript setter functions have the signature:
* <pre>
* <b>this</b>.function(name, newValue, oldValue, exception)
* <b>this</b> is set to the object whose field changed value
* name is the name of the field that changed value
* newValue is the new value of the field
* oldValue was the value of the field before the assignment
* exception is non-null when a value is being reverted
* </pre>
*
* The new value is assigned to the field before the setter is called.
* So, newValue is not strictly needed; it can also be obtained by simply
* reading the value of the field associated with the setter.
*
* Following normal JavaScript conventions, unneeded arguments can be
* omitted from the declaration. So all of the following are valid
* declarations for a setter function:
* <pre>
* function(name, newValue, oldValue) {}
* function(name, newValue) {}
* function(name) {}
* function() {}
* </pre>
*/
public class Setters extends BaseFunction
{
private static final long serialVersionUID = 5777317095106662549L;
private static HashSet<Callable> globalSetters = new HashSet<Callable>();
private static int maxStackDepth = 0;
private static int stackDepth = 0;
private static int rethrowHash = 0;
private Object member;
public Setters(Object member)
{
this.member = member;
}
/**
* Add setter support to a field of an object.
* @return The set of user setters for the field.
*/
public static Set<Callable> init(Value.Observable obj, String name)
{
Object member = obj.lookupFld(name);
if (member == null) {
throw error("can't init " + obj + "." + name, null);
}
return init(obj, member);
}
public static Set<Callable> init(Value.Observable obj, int index)
{
Object member = obj.lookupFld(index);
if (member == null) {
throw error("can't init " + obj + "[" + index + "]", null);
}
return init(obj, member);
}
private static Set<Callable> init(Value.Observable obj, Object member)
{
/* The object 'member' is a field of Proto.fldlist for 'obj' */
Function setter = obj.getSetter(member);
if (setter == null) {
/* if no setter already, create the infrastructure */
setter = new Setters(member);
obj.setSetter(member, setter);
}
else if (!(setter instanceof Setters)) {
/* already has a setter, and it's not me */
throw error("already has a setter", null);
}
return GetSetData.getData(obj, member).setters;
}
/**
* The master setter function called by XDCscript. First calls all
* the global setters, then calls the individual setters registered
* on this field.
*/
@Override
public Object call(Context cx, Scriptable scope, Scriptable thisObj,
Object[] args)
{
Value.Observable obj = (Value.Observable)thisObj;
/* get the new value from the argument list */
Object newValue = args[1];
/* get the old value from the private storage location */
Object oldValue = obj.getFld(member);
if (args.length > 2) {
oldValue = args[2];
}
/* include the old value in the arguments to the setter */
args = new Object[]{args[0], args[1], oldValue, null};
if (GetSet.getDebug()) {
/* clear exception id to reduce possible stacktrace output */
rethrowHash = 0;
/* "entry" trace for this setter */
System.out.println(
"*** maybe setting " + getPName(thisObj, member)
+ " to " + newValue + ", old value " + "is " + oldValue);
}
/* check whether they're different */
if (!equals(newValue, oldValue)) {
/*
* "data" can be null if the member has a setter on
* it in some objects but not all. For example
* in instance configs, or in module configs that
* are inherited from a shared interface.
*/
GetSetData data = (GetSetData)obj.getData(member);
int numberNotified = 0;
try {
/* increment the current stack depth, and check */
if (maxStackDepth > 0) {
if (++stackDepth > maxStackDepth) {
throw error("Setters.maxStackDepth exceeded", null);
}
}
/* can't this be added "outside" this code with a setter? */
if (GetSet.getDebug()) {
System.out.println(
"*** setting " + getPName(thisObj, member)
+ " to " + newValue + " (was: " + oldValue + ")");
}
/* set the private storage location */
obj.setFld(member, newValue);
/* notify all the global setters */
for (Callable f : globalSetters) {
numberNotified++;
f.call(cx, scope, thisObj, args);
}
if (data != null) {
/* notify all the user setters */
for (Callable f : data.setters) {
numberNotified++;
f.call(cx, scope, thisObj, args);
}
}
}
catch (RuntimeException e) {
/*
* if any of the setters throws a runtime exception, for
* example an illegal value, restore the original value
* of the field.
*/
obj.setFld(member, oldValue);
if (GetSet.getDebug()) {
/* don't print rethrown exceptions */
if (rethrowHash != e.hashCode()) {
e.printStackTrace(System.out);
}
System.out.println(
"*** reverting " + getPName(thisObj, member)
+ " to " + oldValue + " (from: " + newValue + ")");
}
/*
* any setters that were notified before the exception,
* tell them about the retraction, including the exception.
*/
Object wrappedException = Context.javaToJS(e, scope);
args = new Object[]{args[0], oldValue, newValue, wrappedException};
int numberRetracted = 0;
/* retract notifications to the global setters */
for (Callable f : globalSetters) {
if (numberRetracted >= numberNotified) {
break;
}
numberRetracted++;
try {
if (GetSet.getDebug()) {
System.out.println(
" *** global setter retraction: calling "
+ getFName(f));
}
f.call(cx, scope, thisObj, args);
}
catch (Exception dbl) {
/* error if setters fail while rolling back */
throw error(
"nested exception while recovering from: " +
e.getMessage(), dbl);
}
}
/* retract notifications to the user setters */
if (data != null) {
for (Callable f : data.setters) {
if (numberRetracted >= numberNotified) {
break;
}
numberRetracted++;
try {
if (GetSet.getDebug()) {
System.out.println(
" *** setter retraction: calling "
+ getFName(f));
}
f.call(cx, scope, thisObj, args);
}
catch (Exception dbl) {
/* error if setters fail while rolling back */
throw error(
"nested exception while recovering from: " +
e.getMessage(), dbl);
}
}
}
/* record last rethrow hash code */
rethrowHash = e.hashCode();
/* and rethrow -- keeps the original stack info */
throw e;
}
finally {
/* decrement the stack depth */
if (maxStackDepth > 0 && --stackDepth < 0) {
/*
* if the stackDepth was cleared inside a setter, then
* the initial stack depth will be off. Reset it.
*/
stackDepth = 0;
}
}
}
/* do whatever this thing does */
return super.call(cx, scope, thisObj, args);
}
/**
* Add a setter that is called when any field changes value.
*/
public static void addGlobal(Callable setter)
{
globalSetters.add(setter);
}
/**
* Get the maximum allowed depth of recursively nested setters.
*/
public static int getMaxStackDepth() {
return maxStackDepth;
}
/**
* Set the maximum allowed depth of recursively nested setters, as
* a debugging aid.
*/
public static void setMaxStackDepth(int maxStackDepth) {
Setters.maxStackDepth = maxStackDepth;
stackDepth = 0;
}
/**
* Helper method to report errors in setter implementations.
*/
private static RuntimeException error(String message, Throwable cause) {
@SuppressWarnings("deprecation")
Throwable e = new JavaScriptException(message);
if (cause != null) {
e.initCause(cause);
}
throw Context.throwAsScriptRuntimeEx(e);
}
/**
* Helper method to check whether two values are equal
* <p/>
*
* Uses compareTo() as well as equals() for {@link XScriptO} objects,
* because they give different answers. For example {@link Extern} objects
* are immutable constants, but the way XDCscript uses them can produce
* multiple copies of essentially the same value.
*/
private static boolean equals(Object x, Object y) {
/* handle any nulls */
if (x == null || y == null) {
return x == y;
}
/* check equals */
if (x.equals(y)) {
return true;
}
/*
* XScriptO objects can be equivalent but not equal, and compareTo()
* will detect this case. This is reliable whenever the $name property
* is assigned automatically, but could conceivably be spoofed using
* classes that allow $name to be assigned, such as Extern. To prevent
* this, also check that the two objects have the same class.
*/
if (x instanceof XScriptO && x.getClass() == y.getClass()) {
if (((XScriptO) x).compareTo(y) == 0) {
return true;
}
}
/* otherwise, no */
return false;
}
/**
* Return name of settable parameter (use in debug trace output)
*/
private static String getPName(Scriptable thisObj, Object member)
{
/* choose a name for thisObj in debug output */
String debugName = thisObj.toString();
debugName = debugName.replaceFirst(".*::", "");
/* choose a name for the field */
String memberName = member instanceof Member ?
((Member)member).getName() : member.toString();
return (debugName + "." + memberName);
}
/**
* Return name of callable function (use in debug trace output)
*/
private static String getFName(Callable f)
{
/* choose a default in case we can't find anything better */
String debugName = f.toString();
/* try getting the JavaScript function name */
try {
if (f instanceof Scriptable) {
Scriptable scriptable = (Scriptable)f;
Object obj = scriptable.get("name", scriptable);
if (obj instanceof String) {
String name = (String) obj;
if (name.length() > 0) {
debugName = name;
}
}
}
}
catch (Exception e) {
}
return debugName;
}
}