| /* --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; |
| } |
| } |