8191905: Add a REMOVE StandardOperation to Dynalink
authorattila
Tue, 12 Dec 2017 14:04:57 +0100
changeset 48244 2bf9071e8dce
parent 48243 ddba406af760
child 48245 be39de5f0315
8191905: Add a REMOVE StandardOperation to Dynalink Reviewed-by: hannesw, sundar
src/jdk.dynalink/share/classes/jdk/dynalink/StandardOperation.java
src/jdk.dynalink/share/classes/jdk/dynalink/beans/BeanLinker.java
src/jdk.dynalink/share/classes/jdk/dynalink/beans/BeansLinker.java
test/nashorn/src/jdk/dynalink/beans/test/BeanLinkerTest.java
--- a/src/jdk.dynalink/share/classes/jdk/dynalink/StandardOperation.java	Tue Dec 12 14:04:05 2017 +0100
+++ b/src/jdk.dynalink/share/classes/jdk/dynalink/StandardOperation.java	Tue Dec 12 14:04:57 2017 +0100
@@ -111,6 +111,15 @@
      */
     SET,
     /**
+     * Removes the value from a namespace defined on an object. Call sites with this
+     * operation should have a signature of
+     * <code>(receiver,&nbsp;name)&rarr;void</code> or
+     * <code>(receiver)&rarr;void</code> when used with {@link NamedOperation},
+     * with all parameters being of any type (either primitive
+     * or reference). This operation must always be used as part of a {@link NamespaceOperation}.
+     */
+    REMOVE,
+    /**
      * Call a callable object. Call sites with this operation should have a
      * signature of <code>(callable,&nbsp;receiver,&nbsp;arguments...)&rarr;value</code>,
      * with all parameters and return type being of any type (either primitive or
--- a/src/jdk.dynalink/share/classes/jdk/dynalink/beans/BeanLinker.java	Tue Dec 12 14:04:05 2017 +0100
+++ b/src/jdk.dynalink/share/classes/jdk/dynalink/beans/BeanLinker.java	Tue Dec 12 14:04:57 2017 +0100
@@ -107,7 +107,8 @@
 
 /**
  * A class that provides linking capabilities for a single POJO class. Normally not used directly, but managed by
- * {@link BeansLinker}.
+ * {@link BeansLinker}. Most of the functionality is provided by the {@link AbstractJavaLinker} superclass; this
+ * class adds length and element operations for arrays and collections.
  */
 class BeanLinker extends AbstractJavaLinker implements TypeBasedGuardingDynamicLinker {
     BeanLinker(final Class<?> clazz) {
@@ -147,6 +148,8 @@
                     return getElementGetter(req.popNamespace());
                 } else if (op == StandardOperation.SET) {
                     return getElementSetter(req.popNamespace());
+                } else if (op == StandardOperation.REMOVE) {
+                    return getElementRemover(req.popNamespace());
                 }
             }
         }
@@ -228,7 +231,7 @@
         // dealing with an array, or a list or map, but hey...
         // Note that for arrays and lists, using LinkerServices.asType() will ensure that any language specific linkers
         // in use will get a chance to perform any (if there's any) implicit conversion to integer for the indices.
-        if(declaredType.isArray()) {
+        if(declaredType.isArray() && arrayMethod != null) {
             return new GuardedInvocationComponentAndCollectionType(
                     createInternalFilteredGuardedInvocationComponent(arrayMethod.apply(declaredType), linkerServices),
                     CollectionType.ARRAY);
@@ -240,7 +243,7 @@
             return new GuardedInvocationComponentAndCollectionType(
                     createInternalFilteredGuardedInvocationComponent(mapMethod, linkerServices),
                     CollectionType.MAP);
-        } else if(clazz.isArray()) {
+        } else if(clazz.isArray() && arrayMethod != null) {
             return new GuardedInvocationComponentAndCollectionType(
                     getClassGuardedInvocationComponent(linkerServices.filterInternalObjects(arrayMethod.apply(clazz)), callSiteType),
                     CollectionType.ARRAY);
@@ -450,7 +453,7 @@
     }
 
     @SuppressWarnings("unused")
-    private static void noOpSetter() {
+    private static void noOp() {
     }
 
     private static final MethodHandle SET_LIST_ELEMENT = Lookup.PUBLIC.findVirtual(List.class, "set",
@@ -459,12 +462,14 @@
     private static final MethodHandle PUT_MAP_ELEMENT = Lookup.PUBLIC.findVirtual(Map.class, "put",
             MethodType.methodType(Object.class, Object.class, Object.class));
 
-    private static final MethodHandle NO_OP_SETTER_2;
-    private static final MethodHandle NO_OP_SETTER_3;
+    private static final MethodHandle NO_OP_1;
+    private static final MethodHandle NO_OP_2;
+    private static final MethodHandle NO_OP_3;
     static {
-        final MethodHandle noOpSetter = Lookup.findOwnStatic(MethodHandles.lookup(), "noOpSetter", void.class);
-        NO_OP_SETTER_2 = dropObjectArguments(noOpSetter, 2);
-        NO_OP_SETTER_3 = dropObjectArguments(noOpSetter, 3);
+        final MethodHandle noOp = Lookup.findOwnStatic(MethodHandles.lookup(), "noOp", void.class);
+        NO_OP_1 = dropObjectArguments(noOp, 1);
+        NO_OP_2 = dropObjectArguments(noOp, 2);
+        NO_OP_3 = dropObjectArguments(noOp, 3);
     }
 
     private GuardedInvocationComponent getElementSetter(final ComponentLinkRequest req) throws Exception {
@@ -503,7 +508,39 @@
             return gic.replaceInvocation(binder.bind(invocation));
         }
 
-        return guardComponentWithRangeCheck(gicact, callSiteType, nextComponent, binder, isFixedKey ? NO_OP_SETTER_2 : NO_OP_SETTER_3);
+        return guardComponentWithRangeCheck(gicact, callSiteType, nextComponent, binder, isFixedKey ? NO_OP_2 : NO_OP_3);
+    }
+
+    private static final MethodHandle REMOVE_LIST_ELEMENT = Lookup.PUBLIC.findVirtual(List.class, "remove",
+            MethodType.methodType(Object.class, int.class));
+
+    private static final MethodHandle REMOVE_MAP_ELEMENT = Lookup.PUBLIC.findVirtual(Map.class, "remove",
+            MethodType.methodType(Object.class, Object.class));
+
+    private GuardedInvocationComponent getElementRemover(final ComponentLinkRequest req) throws Exception {
+        final CallSiteDescriptor callSiteDescriptor = req.getDescriptor();
+        final Object name = req.name;
+        final boolean isFixedKey = name != null;
+        assertParameterCount(callSiteDescriptor, isFixedKey ? 1 : 2);
+        final LinkerServices linkerServices = req.linkerServices;
+        final MethodType callSiteType = callSiteDescriptor.getMethodType();
+        final GuardedInvocationComponent nextComponent = getNextComponent(req);
+
+        final GuardedInvocationComponentAndCollectionType gicact = guardedInvocationComponentAndCollectionType(
+                callSiteType, linkerServices, null, REMOVE_LIST_ELEMENT, REMOVE_MAP_ELEMENT);
+
+        if (gicact == null) {
+            // Can't remove elements for objects that are neither lists, nor maps.
+            return nextComponent;
+        }
+
+        final Object typedName = getTypedName(name, gicact.collectionType == CollectionType.MAP, linkerServices);
+        if (typedName == INVALID_NAME) {
+            return nextComponent;
+        }
+
+        return guardComponentWithRangeCheck(gicact, callSiteType, nextComponent,
+                new Binder(linkerServices, callSiteType, typedName), isFixedKey ? NO_OP_1: NO_OP_2);
     }
 
     private static final MethodHandle GET_COLLECTION_LENGTH = Lookup.PUBLIC.findVirtual(Collection.class, "size",
--- a/src/jdk.dynalink/share/classes/jdk/dynalink/beans/BeansLinker.java	Tue Dec 12 14:04:05 2017 +0100
+++ b/src/jdk.dynalink/share/classes/jdk/dynalink/beans/BeansLinker.java	Tue Dec 12 14:04:57 2017 +0100
@@ -113,6 +113,8 @@
  * <li> expose elements of native Java arrays, {@link java.util.List} and {@link java.util.Map} objects as
  * {@link StandardOperation#GET} and {@link StandardOperation#SET} operations in the
  * {@link StandardNamespace#ELEMENT} namespace;</li>
+ * <li> expose removal of elements of {@link java.util.List} and {@link java.util.Map} objects as
+ * {@link StandardOperation#REMOVE} operation in the {@link StandardNamespace#ELEMENT} namespace;</li>
  * <li>expose a virtual property named {@code length} on Java arrays, {@link java.util.Collection} and
  * {@link java.util.Map} objects;</li>
  * <li>expose {@link StandardOperation#NEW} on instances of {@link StaticClass}
--- a/test/nashorn/src/jdk/dynalink/beans/test/BeanLinkerTest.java	Tue Dec 12 14:04:05 2017 +0100
+++ b/test/nashorn/src/jdk/dynalink/beans/test/BeanLinkerTest.java	Tue Dec 12 14:04:57 2017 +0100
@@ -30,6 +30,7 @@
 import static jdk.dynalink.StandardOperation.CALL;
 import static jdk.dynalink.StandardOperation.GET;
 import static jdk.dynalink.StandardOperation.NEW;
+import static jdk.dynalink.StandardOperation.REMOVE;
 import static jdk.dynalink.StandardOperation.SET;
 
 import java.lang.invoke.CallSite;
@@ -39,7 +40,9 @@
 import java.security.AccessControlException;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import jdk.dynalink.CallSiteDescriptor;
 import jdk.dynalink.DynamicLinker;
 import jdk.dynalink.DynamicLinkerFactory;
@@ -90,6 +93,7 @@
     private static final Operation GET_ELEMENT = GET.withNamespace(ELEMENT);
     private static final Operation GET_METHOD = GET.withNamespace(METHOD);
     private static final Operation SET_ELEMENT = SET.withNamespace(ELEMENT);
+    private static final Operation REMOVE_ELEMENT = REMOVE.withNamespace(ELEMENT);
 
     private static final MethodHandle findThrower(final String name) {
         try {
@@ -121,8 +125,8 @@
             final CallSiteDescriptor desc = req.getCallSiteDescriptor();
             final Operation op = desc.getOperation();
             final Operation baseOp = NamedOperation.getBaseOperation(op);
-            if (baseOp != GET_ELEMENT && baseOp != SET_ELEMENT) {
-                // We only handle GET_ELEMENT and SET_ELEMENT.
+            if (baseOp != GET_ELEMENT && baseOp != SET_ELEMENT && baseOp != REMOVE_ELEMENT) {
+                // We only handle GET_ELEMENT, SET_ELEMENT and REMOVE_ELEMENT.
                 return null;
             }
 
@@ -538,4 +542,85 @@
             }
         }
     }
+
+    @Test(dataProvider = "flags")
+    public void removeElementFromListTest(final boolean publicLookup) throws Throwable {
+        final MethodType mt = MethodType.methodType(void.class, Object.class, int.class);
+        final CallSite cs = createCallSite(publicLookup, REMOVE_ELEMENT, mt);
+
+        final List<Integer> list = new ArrayList<>(List.of(23, 430, -4354));
+
+        cs.getTarget().invoke(list, 1);
+        Assert.assertEquals(list, List.of(23, -4354));
+        cs.getTarget().invoke(list, 1);
+        Assert.assertEquals(list, List.of(23));
+        cs.getTarget().invoke(list, 0);
+        Assert.assertEquals(list, List.of());
+        try {
+            cs.getTarget().invoke(list, -1);
+            throw new RuntimeException("expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException ex) {
+        }
+
+        try {
+            cs.getTarget().invoke(list, list.size());
+            throw new RuntimeException("expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException ex) {
+        }
+    }
+
+    @Test(dataProvider = "flags")
+    public void removeElementFromListWithFixedKeyTest(final boolean publicLookup) throws Throwable {
+        final MethodType mt = MethodType.methodType(void.class, Object.class);
+
+        final List<Integer> list = new ArrayList<>(List.of(23, 430, -4354));
+
+        createCallSite(publicLookup, REMOVE_ELEMENT.named(1), mt).getTarget().invoke(list);
+        Assert.assertEquals(list, List.of(23, -4354));
+        createCallSite(publicLookup, REMOVE_ELEMENT.named(1), mt).getTarget().invoke(list);
+        Assert.assertEquals(list, List.of(23));
+        createCallSite(publicLookup, REMOVE_ELEMENT.named(0), mt).getTarget().invoke(list);
+        Assert.assertEquals(list, List.of());
+        try {
+            createCallSite(publicLookup, REMOVE_ELEMENT.named(-1), mt).getTarget().invoke(list);
+            throw new RuntimeException("expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException ex) {
+        }
+
+        try {
+            createCallSite(publicLookup, REMOVE_ELEMENT.named(list.size()), mt).getTarget().invoke(list);
+            throw new RuntimeException("expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException ex) {
+        }
+    }
+
+    @Test(dataProvider = "flags")
+    public void removeElementFromMapTest(final boolean publicLookup) throws Throwable {
+        final MethodType mt = MethodType.methodType(void.class, Object.class, Object.class);
+        final CallSite cs = createCallSite(publicLookup, REMOVE_ELEMENT, mt);
+
+        final Map<String, String> map = new HashMap<>(Map.of("k1", "v1", "k2", "v2", "k3", "v3"));
+
+        cs.getTarget().invoke(map, "k2");
+        Assert.assertEquals(map, Map.of("k1", "v1", "k3", "v3"));
+        cs.getTarget().invoke(map, "k4");
+        Assert.assertEquals(map, Map.of("k1", "v1", "k3", "v3"));
+        cs.getTarget().invoke(map, "k1");
+        Assert.assertEquals(map, Map.of("k3", "v3"));
+    }
+
+
+    @Test(dataProvider = "flags")
+    public void removeElementFromMapWithFixedKeyTest(final boolean publicLookup) throws Throwable {
+        final MethodType mt = MethodType.methodType(void.class, Object.class);
+
+        final Map<String, String> map = new HashMap<>(Map.of("k1", "v1", "k2", "v2", "k3", "v3"));
+
+        createCallSite(publicLookup, REMOVE_ELEMENT.named("k2"), mt).getTarget().invoke(map);
+        Assert.assertEquals(map, Map.of("k1", "v1", "k3", "v3"));
+        createCallSite(publicLookup, REMOVE_ELEMENT.named("k4"), mt).getTarget().invoke(map);
+        Assert.assertEquals(map, Map.of("k1", "v1", "k3", "v3"));
+        createCallSite(publicLookup, REMOVE_ELEMENT.named("k1"), mt).getTarget().invoke(map);
+        Assert.assertEquals(map, Map.of("k3", "v3"));
+    }
 }