Skip to content

Commit 5ad9ba0

Browse files
Merge branch '67-generic-map-converter' into 'dev'
Java: base classes for generic map converter See merge request objectbox/objectbox-java!69
2 parents 8687cc0 + 218eaef commit 5ad9ba0

File tree

8 files changed

+563
-0
lines changed

8 files changed

+563
-0
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
package io.objectbox.converter;
2+
3+
import io.objectbox.flatbuffers.ArrayReadWriteBuf;
4+
import io.objectbox.flatbuffers.FlexBuffers;
5+
import io.objectbox.flatbuffers.FlexBuffersBuilder;
6+
7+
import java.lang.reflect.Field;
8+
import java.nio.ByteBuffer;
9+
import java.util.ArrayList;
10+
import java.util.HashMap;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.Map.Entry;
14+
import java.util.concurrent.atomic.AtomicReference;
15+
16+
/**
17+
* Converts between {@link Map} properties and byte arrays using FlexBuffers.
18+
* <p>
19+
* All keys must have the same type (see {@link #convertToKey(String)}),
20+
* value types are limited to those supported by FlexBuffers.
21+
* <p>
22+
* If any item requires 64 bits for storage in the FlexBuffers Map/Vector (a large Long, a Double)
23+
* all integers are restored as Long, otherwise Integer.
24+
*/
25+
public abstract class FlexMapConverter implements PropertyConverter<Map<Object, Object>, byte[]> {
26+
27+
private static final AtomicReference<FlexBuffersBuilder> cachedBuilder = new AtomicReference<>();
28+
29+
@Override
30+
public byte[] convertToDatabaseValue(Map<Object, Object> map) {
31+
if (map == null) return null;
32+
33+
FlexBuffersBuilder builder = cachedBuilder.getAndSet(null);
34+
if (builder == null) {
35+
// Note: BUILDER_FLAG_SHARE_KEYS_AND_STRINGS is as fast as no flags for small maps/strings
36+
// and faster for larger maps/strings. BUILDER_FLAG_SHARE_STRINGS is always slower.
37+
builder = new FlexBuffersBuilder(
38+
new ArrayReadWriteBuf(512),
39+
FlexBuffersBuilder.BUILDER_FLAG_SHARE_KEYS_AND_STRINGS
40+
);
41+
}
42+
43+
addMap(builder, null, map);
44+
45+
ByteBuffer buffer = builder.finish();
46+
47+
byte[] out = new byte[buffer.limit()];
48+
buffer.get(out);
49+
50+
// Cache if builder does not consume too much memory
51+
if (buffer.limit() <= 256 * 1024) {
52+
builder.clear();
53+
cachedBuilder.getAndSet(builder);
54+
}
55+
56+
return out;
57+
}
58+
59+
private void addMap(FlexBuffersBuilder builder, String mapKey, Map<Object, Object> map) {
60+
int mapStart = builder.startMap();
61+
62+
for (Entry<Object, Object> entry : map.entrySet()) {
63+
Object value = entry.getValue();
64+
if (entry.getKey() == null || value == null) {
65+
throw new IllegalArgumentException("Map keys or values must not be null");
66+
}
67+
68+
String key = entry.getKey().toString();
69+
if (value instanceof Map) {
70+
//noinspection unchecked
71+
addMap(builder, key, (Map<Object, Object>) value);
72+
} else if (value instanceof List) {
73+
//noinspection unchecked
74+
addVector(builder, key, (List<Object>) value);
75+
} else if (value instanceof String) {
76+
builder.putString(key, (String) value);
77+
} else if (value instanceof Boolean) {
78+
builder.putBoolean(key, (Boolean) value);
79+
} else if (value instanceof Integer) {
80+
builder.putInt(key, (Integer) value);
81+
} else if (value instanceof Long) {
82+
builder.putInt(key, (Long) value);
83+
} else if (value instanceof Float) {
84+
builder.putFloat(key, (Float) value);
85+
} else if (value instanceof Double) {
86+
builder.putFloat(key, (Double) value);
87+
} else if (value instanceof byte[]) {
88+
builder.putBlob(key, (byte[]) value);
89+
} else {
90+
throw new IllegalArgumentException(
91+
"Map values of this type are not supported: " + value.getClass().getSimpleName());
92+
}
93+
}
94+
95+
builder.endMap(mapKey, mapStart);
96+
}
97+
98+
private void addVector(FlexBuffersBuilder builder, String vectorKey, List<Object> list) {
99+
int vectorStart = builder.startVector();
100+
101+
for (Object item : list) {
102+
if (item instanceof Map) {
103+
//noinspection unchecked
104+
addMap(builder, null, (Map<Object, Object>) item);
105+
} else if (item instanceof List) {
106+
//noinspection unchecked
107+
addVector(builder, null, (List<Object>) item);
108+
} else if (item instanceof String) {
109+
builder.putString((String) item);
110+
} else if (item instanceof Boolean) {
111+
builder.putBoolean((Boolean) item);
112+
} else if (item instanceof Integer) {
113+
builder.putInt((Integer) item);
114+
} else if (item instanceof Long) {
115+
builder.putInt((Long) item);
116+
} else if (item instanceof Float) {
117+
builder.putFloat((Float) item);
118+
} else if (item instanceof Double) {
119+
builder.putFloat((Double) item);
120+
} else if (item instanceof byte[]) {
121+
builder.putBlob((byte[]) item);
122+
} else {
123+
throw new IllegalArgumentException(
124+
"List values of this type are not supported: " + item.getClass().getSimpleName());
125+
}
126+
}
127+
128+
builder.endVector(vectorKey, vectorStart, false, false);
129+
}
130+
131+
@Override
132+
public Map<Object, Object> convertToEntityProperty(byte[] databaseValue) {
133+
if (databaseValue == null) return null;
134+
135+
FlexBuffers.Map map = FlexBuffers.getRoot(new ArrayReadWriteBuf(databaseValue, databaseValue.length)).asMap();
136+
137+
return buildMap(map);
138+
}
139+
140+
/**
141+
* Converts a FlexBuffers string map key to the Java map key (e.g. String to Integer).
142+
* <p>
143+
* This required conversion restricts all keys (root and embedded maps) to the same type.
144+
*/
145+
abstract Object convertToKey(String keyValue);
146+
147+
/**
148+
* Returns true if the width in bytes stored in the private parentWidth field of FlexBuffers.Reference is 8.
149+
* Note: FlexBuffers stores all items in a map/vector using the size of the widest item. However,
150+
* an item's size is only as wide as needed, e.g. a 64-bit integer (Java Long, 8 bytes) will be
151+
* reduced to 1 byte if it does not exceed its value range.
152+
*/
153+
protected boolean shouldRestoreAsLong(FlexBuffers.Reference reference) {
154+
try {
155+
Field parentWidthF = reference.getClass().getDeclaredField("parentWidth");
156+
parentWidthF.setAccessible(true);
157+
return (int) parentWidthF.get(reference) == 8;
158+
} catch (Exception e) {
159+
// If thrown, it is likely the FlexBuffers API has changed and the above should be updated.
160+
throw new RuntimeException("FlexMapConverter could not determine FlexBuffers integer bit width.", e);
161+
}
162+
}
163+
164+
private Map<Object, Object> buildMap(FlexBuffers.Map map) {
165+
// As recommended by docs, iterate keys and values vectors in parallel to avoid binary search of key vector.
166+
int entryCount = map.size();
167+
FlexBuffers.KeyVector keys = map.keys();
168+
FlexBuffers.Vector values = map.values();
169+
// Note: avoid HashMap re-hashing by choosing large enough initial capacity.
170+
// From docs: If the initial capacity is greater than the maximum number of entries divided by the load factor,
171+
// no rehash operations will ever occur.
172+
// So set initial capacity based on default load factor 0.75 accordingly.
173+
Map<Object, Object> resultMap = new HashMap<>((int) (entryCount / 0.75 + 1));
174+
for (int i = 0; i < entryCount; i++) {
175+
String rawKey = keys.get(i).toString();
176+
Object key = convertToKey(rawKey);
177+
FlexBuffers.Reference value = values.get(i);
178+
if (value.isMap()) {
179+
resultMap.put(key, buildMap(value.asMap()));
180+
} else if (value.isVector()) {
181+
resultMap.put(key, buildList(value.asVector()));
182+
} else if (value.isString()) {
183+
resultMap.put(key, value.asString());
184+
} else if (value.isBoolean()) {
185+
resultMap.put(key, value.asBoolean());
186+
} else if (value.isInt()) {
187+
if (shouldRestoreAsLong(value)) {
188+
resultMap.put(key, value.asLong());
189+
} else {
190+
resultMap.put(key, value.asInt());
191+
}
192+
} else if (value.isFloat()) {
193+
// Always return as double; if original was float casting will give original value.
194+
resultMap.put(key, value.asFloat());
195+
} else if (value.isBlob()) {
196+
resultMap.put(key, value.asBlob().getBytes());
197+
} else {
198+
throw new IllegalArgumentException(
199+
"Map values of this type are not supported: " + value.getClass().getSimpleName());
200+
}
201+
}
202+
203+
return resultMap;
204+
}
205+
206+
private List<Object> buildList(FlexBuffers.Vector vector) {
207+
int itemCount = vector.size();
208+
List<Object> list = new ArrayList<>(itemCount);
209+
210+
// FlexBuffers uses the byte width of the biggest item to size all items, so only need to check the first.
211+
Boolean shouldRestoreAsLong = null;
212+
213+
for (int i = 0; i < itemCount; i++) {
214+
FlexBuffers.Reference item = vector.get(i);
215+
if (item.isMap()) {
216+
list.add(buildMap(item.asMap()));
217+
} else if (item.isVector()) {
218+
list.add(buildList(item.asVector()));
219+
} else if (item.isString()) {
220+
list.add(item.asString());
221+
} else if (item.isBoolean()) {
222+
list.add(item.asBoolean());
223+
} else if (item.isInt()) {
224+
if (shouldRestoreAsLong == null) {
225+
shouldRestoreAsLong = shouldRestoreAsLong(item);
226+
}
227+
if (shouldRestoreAsLong) {
228+
list.add(item.asLong());
229+
} else {
230+
list.add(item.asInt());
231+
}
232+
} else if (item.isFloat()) {
233+
// Always return as double; if original was float casting will give original value.
234+
list.add(item.asFloat());
235+
} else if (item.isBlob()) {
236+
list.add(item.asBlob().getBytes());
237+
} else {
238+
throw new IllegalArgumentException(
239+
"List values of this type are not supported: " + item.getClass().getSimpleName());
240+
}
241+
}
242+
243+
return list;
244+
}
245+
246+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.objectbox.converter;
2+
3+
/**
4+
* Used to automatically convert {@code Map<Integer, V>}.
5+
*/
6+
public class IntegerFlexMapConverter extends FlexMapConverter {
7+
@Override
8+
Integer convertToKey(String keyValue) {
9+
return Integer.valueOf(keyValue);
10+
}
11+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.objectbox.converter;
2+
3+
import io.objectbox.flatbuffers.FlexBuffers;
4+
5+
/**
6+
* Used to automatically convert {@code Map<Integer, Long>}.
7+
*/
8+
public class IntegerLongMapConverter extends IntegerFlexMapConverter {
9+
@Override
10+
protected boolean shouldRestoreAsLong(FlexBuffers.Reference reference) {
11+
return true; // Restore all integers as java.lang.Long.
12+
}
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.objectbox.converter;
2+
3+
/**
4+
* Used to automatically convert {@code Map<Long, V>}.
5+
*/
6+
public class LongFlexMapConverter extends FlexMapConverter {
7+
@Override
8+
Object convertToKey(String keyValue) {
9+
return Long.valueOf(keyValue);
10+
}
11+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.objectbox.converter;
2+
3+
import io.objectbox.flatbuffers.FlexBuffers;
4+
5+
/**
6+
* Used to automatically convert {@code Map<Long, Long>}.
7+
*/
8+
public class LongLongMapConverter extends LongFlexMapConverter {
9+
@Override
10+
protected boolean shouldRestoreAsLong(FlexBuffers.Reference reference) {
11+
return true; // Restore all integers as java.lang.Long.
12+
}
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.objectbox.converter;
2+
3+
/**
4+
* Used to automatically convert {@code Map<String, V>}.
5+
*/
6+
public class StringFlexMapConverter extends FlexMapConverter {
7+
@Override
8+
Object convertToKey(String keyValue) {
9+
return keyValue;
10+
}
11+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.objectbox.converter;
2+
3+
import io.objectbox.flatbuffers.FlexBuffers;
4+
5+
/**
6+
* Used to automatically convert {@code Map<String, Long>}.
7+
*/
8+
public class StringLongMapConverter extends StringFlexMapConverter {
9+
@Override
10+
protected boolean shouldRestoreAsLong(FlexBuffers.Reference reference) {
11+
return true; // Restore all integers as java.lang.Long.
12+
}
13+
}

0 commit comments

Comments
 (0)