Skip to content

Commit 1622e73

Browse files
committed
Make mutating operations on wrapped maps work
Currently, wrapped maps use the passed-in Map as-is. This produces intuitive results when the Map is mutated, but as soon as you try to modify the DynamicObject, you get cryptic errors due to attempts to use this Map as an IPersistentMap or IObj (in the case of metadata operations). This change introduces a wrapper which proxies non-mutating calls through to the underlying map directly, but which creates a copy of the map (as a PersistentHashMap) if mutating operations are performed. I considered simply having wrap() copy the map directly, but I felt that the name 'wrap' would be seen to imply that mutations on the underlying map would be reflected in the wrapped DynamicObject, so I chose this route instead.
1 parent faafd4f commit 1622e73

File tree

4 files changed

+199
-2
lines changed

4 files changed

+199
-2
lines changed

src/main/java/com/github/rschmitt/dynamicobject/DynamicObject.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ static <T> Stream<T> deserializeFressianStream(InputStream is, Class<T> type) {
161161
}
162162

163163
/**
164-
* Use the supplied {@code map} to back an instance of {@code type}.
164+
* Use the supplied {@code map} to back an instance of {@code type}. The map will be copied upon any modification
165+
* attempt, but until then will reflect changes made to the underlying map.
165166
*/
166167
static <D extends DynamicObject<D>> D wrap(Map map, Class<D> type) {
167168
return Instances.wrap(map, type);

src/main/java/com/github/rschmitt/dynamicobject/internal/Instances.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.github.rschmitt.dynamicobject.internal;
22

3+
import clojure.lang.IPersistentMap;
34
import com.github.rschmitt.dynamicobject.DynamicObject;
45
import net.fushizen.invokedynamic.proxy.DynamicProxy;
56

@@ -23,7 +24,15 @@ public static <D extends DynamicObject<D>> D wrap(Map map, Class<D> type) {
2324
if (map instanceof DynamicObject)
2425
return type.cast(map);
2526

26-
return createIndyProxy(map, type);
27+
return createIndyProxy(convertMap(map), type);
28+
}
29+
30+
private static Map convertMap(Map map) {
31+
if (map instanceof IPersistentMap) {
32+
return map;
33+
}
34+
35+
return (Map) WrappingMap.create(map);
2736
}
2837

2938
private static <D extends DynamicObject<D>> D createIndyProxy(Map map, Class<D> type) {
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package com.github.rschmitt.dynamicobject.internal;
2+
3+
import clojure.lang.Cons;
4+
import clojure.lang.IMeta;
5+
import clojure.lang.IObj;
6+
import clojure.lang.IPersistentMap;
7+
import clojure.lang.PersistentHashMap;
8+
import com.github.rschmitt.collider.ClojureMap;
9+
import net.fushizen.invokedynamic.proxy.DynamicProxy;
10+
11+
import java.lang.invoke.CallSite;
12+
import java.lang.invoke.ConstantCallSite;
13+
import java.lang.invoke.MethodHandle;
14+
import java.lang.invoke.MethodHandles;
15+
import java.lang.invoke.MethodType;
16+
import java.lang.reflect.Method;
17+
import java.util.Map;
18+
19+
import static java.lang.invoke.MethodHandles.publicLookup;
20+
import static java.lang.invoke.MethodType.methodType;
21+
22+
/**
23+
* A map class that wraps some other Map; however, it also implements IPersistentMap and IObj, creating a copy of the
24+
* original map when IPersistentMap or IObj methods such as assoc are invoked.
25+
*/
26+
abstract class WrappingMap implements IMeta {
27+
private static final IPersistentMap EMPTY_MAP = PersistentHashMap.create();
28+
29+
protected final Map backingMap;
30+
31+
protected WrappingMap(Map backingMap) {
32+
this.backingMap = backingMap;
33+
}
34+
35+
@Override
36+
public IPersistentMap meta() {
37+
// Avoid copying the map if we're doing a read-only metadata access.
38+
return EMPTY_MAP;
39+
}
40+
41+
static Map create(Map other) {
42+
try {
43+
return (Map)proxy_ctor.invokeExact(other);
44+
} catch (Throwable t) {
45+
throw new Error("unexpected exception", t);
46+
}
47+
}
48+
49+
private static final MethodHandle get_backingMap;
50+
private static final MethodHandle proxy_ctor;
51+
52+
static {
53+
try {
54+
get_backingMap = MethodHandles.lookup().findGetter(WrappingMap.class, "backingMap", Map.class);
55+
proxy_ctor = DynamicProxy.builder()
56+
.withConstructor(Map.class)
57+
.withSuperclass(WrappingMap.class)
58+
.withInterfaces(IPersistentMap.class, IObj.class, Map.class)
59+
.withInvocationHandler(WrappingMap::invocationHandler)
60+
.build()
61+
.constructor()
62+
.asType(methodType(Map.class, Map.class));
63+
} catch (Exception e) {
64+
throw new Error(e);
65+
}
66+
}
67+
68+
private static CallSite invocationHandler(
69+
MethodHandles.Lookup lookup,
70+
String methodName,
71+
MethodType methodType,
72+
MethodHandle superHandle
73+
) throws Throwable {
74+
// Forward calls that are overridden on WrappingMap to that implementation
75+
try {
76+
Method m = WrappingMap.class.getDeclaredMethod(methodName, methodType.dropParameterTypes(0, 1).parameterArray());
77+
78+
// since the method exists, we can just use superHandle
79+
return new ConstantCallSite(superHandle.asType(methodType));
80+
} catch (NoSuchMethodException e) {
81+
// continue
82+
}
83+
84+
CallSite result;
85+
86+
// Forward calls that are declared on Map, or Object to the backing map.
87+
result = forwardCalls(Map.class, methodName, methodType);
88+
if (result != null) return result;
89+
90+
result = forwardCalls(Object.class, methodName, methodType);
91+
if (result != null) return result;
92+
93+
// Any other calls are IPersistentMap calls. We'll want to construct a PersistentHashMap and reinvoke the call
94+
// on it.
95+
MethodHandle makeMap = MethodHandles.lookup().findVirtual(WrappingMap.class, "createPersistentMap", methodType(IPersistentMap.class));
96+
97+
MethodType targetMethodType = methodType.dropParameterTypes(0, 1);
98+
99+
MethodHandle target;
100+
try {
101+
target = publicLookup().findVirtual(IPersistentMap.class, methodName, targetMethodType);
102+
} catch (NoSuchMethodException e) {
103+
// It's not on IPersistentMap, so try IObj instead
104+
target = publicLookup().findVirtual(IObj.class, methodName, targetMethodType);
105+
// If we're successful, we need to do an asType cast since IPersistentMap doesn't extend IObj
106+
makeMap = makeMap.asType(methodType(IObj.class, WrappingMap.class));
107+
}
108+
109+
MethodHandle createMapAndForward = MethodHandles.filterArguments(target, 0, makeMap);
110+
111+
return new ConstantCallSite(createMapAndForward.asType(methodType));
112+
}
113+
114+
private static CallSite forwardCalls(
115+
Class<?> klass,
116+
String methodName,
117+
MethodType methodType
118+
) throws Throwable {
119+
try {
120+
MethodHandle mapHandle = publicLookup().findVirtual(klass, methodName, methodType.changeParameterType(0, Map.class));
121+
// ok, this is a call to the class in question, we just need to look up the backing map and invoke the
122+
// method on it instead
123+
MethodHandle combinedHandle = MethodHandles.filterArguments(mapHandle, 0, get_backingMap);
124+
125+
return new ConstantCallSite(combinedHandle.asType(methodType));
126+
} catch (NoSuchMethodException e) {
127+
return null;
128+
}
129+
}
130+
131+
@SuppressWarnings("unused") // invoked by reflection
132+
protected IPersistentMap createPersistentMap() {
133+
return PersistentHashMap.create(backingMap);
134+
}
135+
136+
@SuppressWarnings("unused") // invoked by reflection
137+
private static Object throwUnsupported() {
138+
throw new UnsupportedOperationException();
139+
}
140+
}

src/test/java/com/github/rschmitt/dynamicobject/MapTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static java.lang.String.format;
66
import static org.junit.Assert.assertEquals;
77
import static org.junit.Assert.assertFalse;
8+
import static org.junit.Assert.assertNull;
89

910
import org.junit.After;
1011
import org.junit.Before;
@@ -13,6 +14,8 @@
1314
import clojure.lang.EdnReader;
1415
import clojure.lang.PersistentHashMap;
1516

17+
import java.util.HashMap;
18+
1619
public class MapTest {
1720
static final String SimpleEdn = "{:str \"expected value\", :i 4, :d 3.14}";
1821
static final String NestedEdn = format("{:version 1, :simple %s}", SimpleEdn);
@@ -52,11 +55,55 @@ public void unknownFieldsAreSerialized() {
5255
assertEquals(TaggedEdn, actualEdn);
5356
}
5457

58+
@Test
59+
public void wrappedMapGettersWork() throws Exception {
60+
HashMap<Object, Object> map = new HashMap<>();
61+
map.put("foo", "bar");
62+
63+
TestObject obj = DynamicObject.wrap(map, TestObject.class);
64+
65+
assertEquals("bar", obj.foo());
66+
}
67+
68+
69+
@Test
70+
public void wrappedMapSettersWork() throws Exception {
71+
HashMap<Object, Object> map = new HashMap<>();
72+
map.put("foo", "bar");
73+
74+
TestObject obj = DynamicObject.wrap(map, TestObject.class);
75+
obj = obj.withFoo("quux");
76+
77+
assertEquals("quux", obj.foo());
78+
}
79+
80+
@Test
81+
public void wrappedMapMetaWorks() throws Exception {
82+
HashMap<Object, Object> map = new HashMap<>();
83+
map.put("foo", "bar");
84+
85+
TestObject obj = DynamicObject.wrap(map, TestObject.class);
86+
87+
assertNull(obj.getMeta());
88+
obj = obj.withMeta("x");
89+
assertEquals("x", obj.getMeta());
90+
91+
assertEquals(1, obj.getMap().size());
92+
}
93+
5594
private void binaryRoundTrip(Object expected) {
5695
Object actual = DynamicObject.fromFressianByteArray(DynamicObject.toFressianByteArray(expected));
5796
assertEquals(expected, actual);
5897
}
5998

6099
public interface EmptyObject extends DynamicObject<EmptyObject> {
61100
}
101+
102+
public interface TestObject extends DynamicObject<TestObject> {
103+
@Key("foo") String foo();
104+
@Key("foo") TestObject withFoo(String foo);
105+
106+
@Meta @Key(":meta") String getMeta();
107+
@Meta @Key(":meta") TestObject withMeta(String meta);
108+
}
62109
}

0 commit comments

Comments
 (0)