зеркало из https://github.com/mozilla/rhino.git
adding new type of Slot called OwnerAwareLambdaSlot, that allows defining getter / setter properties using Lambda functions that accept owner object ('this') as one of parameters, thus allowing to implement properties that require access to instance fields etc.
This commit is contained in:
Родитель
f63941c7e4
Коммит
6e4fe32c59
|
@ -6,6 +6,9 @@
|
|||
|
||||
package org.mozilla.javascript;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* This class implements a JavaScript function that may be used as a constructor by delegating to an
|
||||
* interface that can be easily implemented as a lambda. The LambdaFunction class may be used to add
|
||||
|
@ -120,6 +123,17 @@ public class LambdaConstructor extends LambdaFunction {
|
|||
proto.defineProperty(key, value, attributes);
|
||||
}
|
||||
|
||||
public void definePrototypeProperty(String name, java.util.function.Function<Scriptable, Object> getter, int attributes) {
|
||||
ScriptableObject proto = getPrototypeScriptable();
|
||||
proto.defineProperty(name, getter, null, attributes );
|
||||
}
|
||||
|
||||
public void definePrototypeProperty(String name, Function<Scriptable, Object> getter, BiConsumer<Scriptable, Object> setter, int attributes) {
|
||||
ScriptableObject proto = getPrototypeScriptable();
|
||||
proto.defineProperty(name, getter, setter, attributes );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Define a function property directly on the constructor that is implemented under the covers
|
||||
* by a LambdaFunction.
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package org.mozilla.javascript;
|
||||
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* A specialized property accessor using lambda functions, similar to {@link LambdaSlot},
|
||||
* but allows defining properties with getter and setter lambdas that require access to the owner object ('this').
|
||||
* This enables the implementation of properties that can access instance fields of the owner.
|
||||
*
|
||||
* Unlike {@link LambdaSlot}, Lambda functions used to define getter and setter logic require the owner's
|
||||
* `Scriptable` object as one of the parameters.
|
||||
* This is particularly useful for implementing properties that behave like standard JavaScript properties,
|
||||
* but are implemented with native functionality without the need for reflection.
|
||||
*/
|
||||
public class OwnerAwareLambdaSlot extends Slot {
|
||||
private transient Function<Scriptable, Object> getter;
|
||||
private transient BiConsumer<Scriptable, Object> setter;
|
||||
OwnerAwareLambdaSlot(Object name, int index) {
|
||||
super(name, index, 0);
|
||||
}
|
||||
|
||||
OwnerAwareLambdaSlot(Slot oldSlot) {
|
||||
super(oldSlot);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isValueSlot() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
ScriptableObject getPropertyDescriptor(Context cx, Scriptable scope) {
|
||||
ScriptableObject desc = (ScriptableObject) cx.newObject(scope);
|
||||
if (getter != null) {
|
||||
desc.defineProperty("get",
|
||||
new LambdaFunction(scope, "get " + super.name, 0, (cx1, scope1, thisObj, args) ->
|
||||
getter.apply(thisObj)), ScriptableObject.EMPTY);
|
||||
}
|
||||
|
||||
if (setter != null) {
|
||||
desc.defineProperty("set",
|
||||
new LambdaFunction(scope, "set " + super.name, 1, (cx1, scope1, thisObj, args) ->
|
||||
{
|
||||
setter.accept(thisObj, args[0]);
|
||||
return Undefined.instance;
|
||||
}), ScriptableObject.EMPTY);
|
||||
}
|
||||
desc.setCommonDescriptorProperties(getAttributes(), false);
|
||||
return desc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setValue(Object value, Scriptable scope, Scriptable owner, boolean isStrict) {
|
||||
if (setter != null) {
|
||||
setter.accept(owner, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isStrict) {
|
||||
// in strict mode
|
||||
throw ScriptRuntime.typeErrorById("msg.modify.readonly", name);
|
||||
} else {
|
||||
super.setValue(value, scope, owner, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue(Scriptable owner) {
|
||||
if (getter != null) {
|
||||
return getter.apply(owner);
|
||||
}
|
||||
Object v = super.getValue(owner);
|
||||
return v == null ? Undefined.instance : v;
|
||||
}
|
||||
|
||||
public void setGetter(Function<Scriptable, Object> getter) {
|
||||
this.getter = getter;
|
||||
}
|
||||
|
||||
public void setSetter(BiConsumer<Scriptable, Object> setter) {
|
||||
this.setter = setter;
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import org.mozilla.javascript.ScriptRuntime.StringIdOrIndex;
|
||||
|
@ -1690,6 +1691,35 @@ public abstract class ScriptableObject
|
|||
slot.setter = setter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a property on this object that is implemented using lambda functions accepting Scriptable `this` object
|
||||
* as first parameter. Unlike with `defineProperty(String name, Supplier<Object> getter, Consumer<Object> setter, int attributes)`
|
||||
* where getter and setter need to have access to target object instance, this allows for defining properties on
|
||||
* LambdaConstructor prototype providing getter and setter logic with java instance methods.
|
||||
* If a property with the same name already exists, then it will be replaced. This property will appear to the
|
||||
* JavaScript user exactly like any other property -- unlike Function properties and those based
|
||||
* on reflection, the property descriptor will only reflect the value as defined by this
|
||||
* function.
|
||||
*
|
||||
* @param name the name of the property
|
||||
* @param getter a function that given Scriptable `this` returns the value of the property. If null, throws typeError
|
||||
* @param setter a function that Scriptable `this` and a value sets the value of the property, by calling appropriate
|
||||
* method on `this`. If null, then the value will be
|
||||
* set directly and may not be retrieved by the getter.
|
||||
* @param attributes the attributes to set on the property
|
||||
*/
|
||||
public void defineProperty(
|
||||
String name, java.util.function.Function<Scriptable, Object> getter, BiConsumer<Scriptable, Object> setter, int attributes) {
|
||||
if (getter == null && setter == null)
|
||||
throw ScriptRuntime.typeError("at least one of {getter, setter} is required");
|
||||
|
||||
OwnerAwareLambdaSlot slot = slotMap.compute(name, 0, ScriptableObject::ensureOwnerAwareLambdaSlot);
|
||||
slot.setAttributes(attributes);
|
||||
|
||||
slot.setGetter(getter);
|
||||
slot.setSetter(setter);
|
||||
}
|
||||
|
||||
protected void checkPropertyDefinition(ScriptableObject desc) {
|
||||
Object getter = getProperty(desc, "get");
|
||||
if (getter != NOT_FOUND && getter != Undefined.instance && !(getter instanceof Callable)) {
|
||||
|
@ -2695,6 +2725,16 @@ public abstract class ScriptableObject
|
|||
}
|
||||
}
|
||||
|
||||
private static OwnerAwareLambdaSlot ensureOwnerAwareLambdaSlot(Object name, int index, Slot existing) {
|
||||
if (existing == null) {
|
||||
return new OwnerAwareLambdaSlot(name, index);
|
||||
} else if (existing instanceof OwnerAwareLambdaSlot) {
|
||||
return (OwnerAwareLambdaSlot) existing;
|
||||
} else {
|
||||
return new OwnerAwareLambdaSlot(existing);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeObject(ObjectOutputStream out) throws IOException {
|
||||
out.defaultWriteObject();
|
||||
final long stamp = slotMap.readLock();
|
||||
|
|
|
@ -0,0 +1,319 @@
|
|||
package org.mozilla.javascript.tests;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mozilla.javascript.Context;
|
||||
import org.mozilla.javascript.ContextFactory;
|
||||
import org.mozilla.javascript.EcmaError;
|
||||
import org.mozilla.javascript.LambdaConstructor;
|
||||
import org.mozilla.javascript.Scriptable;
|
||||
import org.mozilla.javascript.ScriptableObject;
|
||||
import org.mozilla.javascript.Undefined;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mozilla.javascript.ScriptableObject.DONTENUM;
|
||||
import static org.mozilla.javascript.tests.OwnerAwareLambdaSlotTest.StatusHolder.self;
|
||||
|
||||
public class OwnerAwareLambdaSlotTest {
|
||||
private Context cx;
|
||||
private ScriptableObject scope;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
cx = Context.enter();
|
||||
scope = cx.initStandardObjects();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
Context.exit();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGetterProperty() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
(thisObj) -> self(thisObj).getStatus(),
|
||||
(thisObj, value) ->
|
||||
self(thisObj).setStatus(value),
|
||||
DONTENUM);
|
||||
|
||||
Object getterResult = cx.evaluateString(scope, "s = new StatusHolder('InProgress'); s.status", "source", 1, null);
|
||||
assertEquals("InProgress", getterResult);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThrowIfNeitherGetterOrSetterAreDefined() {
|
||||
var error = assertThrows(EcmaError.class, () ->
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
null,
|
||||
null,
|
||||
DONTENUM));
|
||||
assertTrue(error.toString().contains("at least one of {getter, setter} is required"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanUpdateValueUsingSetter() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
(thisObj) -> self(thisObj).getStatus(),
|
||||
(thisObj, value) ->
|
||||
self(thisObj).setStatus(value),
|
||||
DONTENUM);
|
||||
|
||||
Object getterResult = cx.evaluateString(scope, "s = new StatusHolder('InProgress'); s.status", "source", 1, null);
|
||||
assertEquals("InProgress", getterResult);
|
||||
|
||||
Object setResult = cx.evaluateString(scope, "s.status = 'DONE';", "source", 1, null);
|
||||
|
||||
Object newStatus = cx.evaluateString(scope, "s.status", "source", 1, null);
|
||||
assertEquals( "NewStatus: DONE", newStatus);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlyGetterCanBeAccessed() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status", (thisObj) -> self(thisObj).getStatus(), DONTENUM);
|
||||
|
||||
Object getterResult = cx.evaluateString(scope, "new StatusHolder('OK').status", "source", 1, null);
|
||||
assertEquals("OK", getterResult);
|
||||
|
||||
Object hiddenFieldResult = cx.evaluateString(scope, "new StatusHolder('OK').hiddenStatus", "source", 1, null);
|
||||
assertEquals("fields not explicitly defined as properties should return undefined", Undefined.instance, hiddenFieldResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhenNoSetterDefined_InStrictMode_WillThrowException() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
(thisObj) -> self(thisObj).getStatus(),
|
||||
DONTENUM);
|
||||
Object getterResult = cx.evaluateString(scope, "s = new StatusHolder('Constant'); s.status", "source", 1, null);
|
||||
assertEquals("Constant", getterResult);
|
||||
|
||||
var error = assertThrows(EcmaError.class, () ->
|
||||
cx.evaluateString(scope,
|
||||
"\"use strict\"; s.status = 'DONE'; s.status", "source", 1, null));
|
||||
assertTrue(error.toString().contains("Cannot modify readonly property: status."));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhenNoSetterDefined_InNormalMode_NoErrorButValueIsNotChanged() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
(thisObj) -> self(thisObj).getStatus(),
|
||||
DONTENUM);
|
||||
|
||||
Object getterResult = cx.evaluateString(scope, "s = new StatusHolder('Constant'); s.status", "source", 1, null);
|
||||
assertEquals("Constant", getterResult);
|
||||
|
||||
Object setResult = cx.evaluateString(scope, "s.status = 'DONE'; s.status", "source", 1, null);
|
||||
assertEquals("status won't be changed", "Constant", setResult);
|
||||
|
||||
Object shObj = cx.evaluateString(scope, "s", "source", 1, null);
|
||||
var statusHolder = (StatusHolder) shObj;
|
||||
assertEquals("Constant", statusHolder.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetterOnly_WillModifyUnderlyingValue() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
null,
|
||||
(thisObj, value) ->
|
||||
self(thisObj).setStatus(value),
|
||||
DONTENUM);
|
||||
cx.evaluateString(scope, "s = new StatusHolder('Constant')", "source", 1, null);
|
||||
|
||||
cx.evaluateString(scope, "s.status = 'DONE'; s.status", "source", 1, null);
|
||||
|
||||
|
||||
Object newStatus = cx.evaluateString(scope, "s.status", "source", 1, null);
|
||||
assertEquals( Undefined.instance, newStatus);
|
||||
Object shObj = cx.evaluateString(scope, "s", "source", 1, null);
|
||||
var statusHolder = (StatusHolder) shObj;
|
||||
assertEquals("NewStatus: DONE", statusHolder.getStatus());
|
||||
}
|
||||
|
||||
|
||||
|
||||
// using getOwnPropertyDescriptor to access property
|
||||
|
||||
@Test
|
||||
public void testGetterUsing_getOwnPropertyDescriptor() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
(thisObj) -> self(thisObj).getStatus(), DONTENUM);
|
||||
|
||||
Object result = cx.evaluateString(scope, "s = new StatusHolder('InProgress');" +
|
||||
"f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
|
||||
"f.get.call(s)",
|
||||
"source", 1, null);
|
||||
assertEquals("InProgress", result);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSetterOnlyUsing_getOwnPropertyDescriptor() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
null,
|
||||
(thisObj, value) ->
|
||||
self(thisObj).setStatus(value), DONTENUM);
|
||||
|
||||
Object shObj = cx.evaluateString(scope,
|
||||
"s = new StatusHolder('InProgress');" +
|
||||
"f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
|
||||
"f.set.call(s, 'DONE');" +
|
||||
"s",
|
||||
"source", 1, null);
|
||||
var statusHolder = (StatusHolder) shObj;
|
||||
assertEquals("NewStatus: DONE", statusHolder.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetValueUsing_getOwnPropertyDescriptor() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
(thisObj) -> self(thisObj).getStatus(),
|
||||
(thisObj, value) ->
|
||||
self(thisObj).setStatus(value), DONTENUM);
|
||||
|
||||
Object result = cx.evaluateString(scope,
|
||||
"s = new StatusHolder('InProgress');" +
|
||||
"f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
|
||||
"f.set.call(s, 'DONE');" +
|
||||
"s.status",
|
||||
"source", 1, null);
|
||||
assertEquals("Status with prefix", "NewStatus: DONE", result);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testSetterOnlyUsing_getOwnPropertyDescriptor_ErrorOnGet() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
null,
|
||||
(thisObj, value) ->
|
||||
self(thisObj).setStatus(value), DONTENUM);
|
||||
|
||||
var error = assertThrows(EcmaError.class, () ->
|
||||
cx.evaluateString(scope,
|
||||
"var s = new StatusHolder('InProgress');" +
|
||||
"var f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
|
||||
"f.get.call(s)",
|
||||
"source", 1, null));
|
||||
assertTrue(error.toString().contains("Cannot call method \"call\" of undefined"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetterOnlyUsing_getOwnPropertyDescriptor_InStrictMode_ErrorOnGet() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
null,
|
||||
(thisObj, value) ->
|
||||
self(thisObj).setStatus(value), DONTENUM);
|
||||
|
||||
var error = assertThrows(EcmaError.class, () ->
|
||||
cx.evaluateString(scope,
|
||||
"\"use strict\";"+
|
||||
"var s = new StatusHolder('InProgress');" +
|
||||
"var f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
|
||||
"f.get.call(s)",
|
||||
"source", 1, null));
|
||||
assertTrue(error.toString().contains("Cannot call method \"call\" of undefined"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetterOnlyUsing_getOwnPropertyDescriptor_ErrorOnSet() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
(thisObj) -> self(thisObj).getStatus(), DONTENUM);
|
||||
|
||||
|
||||
var error = assertThrows(EcmaError.class, () ->
|
||||
cx.evaluateString(scope,
|
||||
"var s = new StatusHolder('InProgress');" +
|
||||
"var f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
|
||||
"f.set.call(s, 'DONE');" +
|
||||
"s.status",
|
||||
"source", 1, null));
|
||||
assertTrue(error.toString().contains("Cannot call method \"call\" of undefined"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetterOnlyUsing_getOwnPropertyDescriptor_InStrictMode_ErrorOnSet() {
|
||||
StatusHolder
|
||||
.init(scope)
|
||||
.definePrototypeProperty("status",
|
||||
(thisObj) -> self(thisObj).getStatus(), DONTENUM);
|
||||
|
||||
|
||||
var error = assertThrows(EcmaError.class, () ->
|
||||
cx.evaluateString(scope,
|
||||
"\"use strict\";" +
|
||||
"var s = new StatusHolder('InProgress');" +
|
||||
"var f = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(s), 'status');" +
|
||||
"f.set.call(s, 'DONE');" +
|
||||
"s.status",
|
||||
"source", 1, null));
|
||||
assertTrue(error.toString().contains("Cannot call method \"call\" of undefined"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
static class StatusHolder extends ScriptableObject {
|
||||
private String status;
|
||||
private final String hiddenStatus;
|
||||
|
||||
static LambdaConstructor init(Scriptable scope) {
|
||||
LambdaConstructor constructor = new LambdaConstructor(scope, "StatusHolder", 1,
|
||||
LambdaConstructor.CONSTRUCTOR_NEW,
|
||||
(cx, scope1, args) -> new StatusHolder((String)args[0]));
|
||||
|
||||
ScriptableObject.defineProperty(scope, "StatusHolder", constructor, DONTENUM);
|
||||
return constructor;
|
||||
}
|
||||
|
||||
static StatusHolder self(Scriptable thisObj) {
|
||||
return LambdaConstructor.convertThisObject(thisObj, StatusHolder.class);
|
||||
}
|
||||
|
||||
StatusHolder(String status) {
|
||||
this.status = status;
|
||||
this.hiddenStatus = "NotQuiteReady";
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClassName() {
|
||||
return "StatusHolder";
|
||||
}
|
||||
|
||||
public void setStatus(Object value) {
|
||||
this.status = "NewStatus: "+ (String)value;
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче