Skip to content

Commit 59f6633

Browse files
Add FlexObjectConverter.
1 parent 342b96a commit 59f6633

File tree

6 files changed

+403
-5
lines changed

6 files changed

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

tests/objectbox-java-test/src/main/java/io/objectbox/TestEntity.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class TestEntity {
5252
/** In "real" entity would be annotated with @Unsigned. */
5353
private long simpleLongU;
5454
private Map<String, Object> stringObjectMap;
55+
private Object flexProperty;
5556

5657
transient boolean noArgsConstructorCalled;
5758

@@ -66,7 +67,8 @@ public TestEntity(long id) {
6667
public TestEntity(long id, boolean simpleBoolean, byte simpleByte, short simpleShort, int simpleInt,
6768
long simpleLong, float simpleFloat, double simpleDouble, String simpleString,
6869
byte[] simpleByteArray, String[] simpleStringArray, List<String> simpleStringList,
69-
short simpleShortU, int simpleIntU, long simpleLongU, Map<String, Object> stringObjectMap) {
70+
short simpleShortU, int simpleIntU, long simpleLongU, Map<String, Object> stringObjectMap,
71+
Object flexProperty) {
7072
this.id = id;
7173
this.simpleBoolean = simpleBoolean;
7274
this.simpleByte = simpleByte;
@@ -83,6 +85,7 @@ public TestEntity(long id, boolean simpleBoolean, byte simpleByte, short simpleS
8385
this.simpleIntU = simpleIntU;
8486
this.simpleLongU = simpleLongU;
8587
this.stringObjectMap = stringObjectMap;
88+
this.flexProperty = flexProperty;
8689
if (STRING_VALUE_THROW_IN_CONSTRUCTOR.equals(simpleString)) {
8790
throw new RuntimeException(EXCEPTION_IN_CONSTRUCTOR_MESSAGE);
8891
}
@@ -226,6 +229,16 @@ public TestEntity setStringObjectMap(Map<String, Object> stringObjectMap) {
226229
return this;
227230
}
228231

232+
@Nullable
233+
public Object getFlexProperty() {
234+
return flexProperty;
235+
}
236+
237+
public TestEntity setFlexProperty(@Nullable Object flexProperty) {
238+
this.flexProperty = flexProperty;
239+
return this;
240+
}
241+
229242
@Override
230243
public String toString() {
231244
return "TestEntity{" +
@@ -245,6 +258,7 @@ public String toString() {
245258
", simpleIntU=" + simpleIntU +
246259
", simpleLongU=" + simpleLongU +
247260
", stringObjectMap=" + stringObjectMap +
261+
", flexProperty=" + flexProperty +
248262
", noArgsConstructorCalled=" + noArgsConstructorCalled +
249263
'}';
250264
}

tests/objectbox-java-test/src/main/java/io/objectbox/TestEntityCursor.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package io.objectbox;
1818

1919
import io.objectbox.annotation.apihint.Internal;
20+
import io.objectbox.converter.FlexObjectConverter;
2021
import io.objectbox.converter.StringFlexMapConverter;
2122
import io.objectbox.internal.CursorFactory;
2223

@@ -45,6 +46,7 @@ public Cursor<TestEntity> createCursor(io.objectbox.Transaction tx, long cursorH
4546
private static final TestEntity_.TestEntityIdGetter ID_GETTER = TestEntity_.__ID_GETTER;
4647

4748
private final StringFlexMapConverter stringObjectMapConverter = new StringFlexMapConverter();
49+
private final FlexObjectConverter flexPropertyConverter = new FlexObjectConverter();
4850

4951
private final static int __ID_simpleBoolean = TestEntity_.simpleBoolean.id;
5052
private final static int __ID_simpleByte = TestEntity_.simpleByte.id;
@@ -61,6 +63,7 @@ public Cursor<TestEntity> createCursor(io.objectbox.Transaction tx, long cursorH
6163
private final static int __ID_simpleIntU = TestEntity_.simpleIntU.id;
6264
private final static int __ID_simpleLongU = TestEntity_.simpleLongU.id;
6365
private final static int __ID_stringObjectMap = TestEntity_.stringObjectMap.id;
66+
private final static int __ID_flexProperty = TestEntity_.flexProperty.id;
6467

6568
public TestEntityCursor(io.objectbox.Transaction tx, long cursor, BoxStore boxStore) {
6669
super(tx, cursor, TestEntity_.__INSTANCE, boxStore);
@@ -96,12 +99,14 @@ public final long put(TestEntity entity) {
9699
int __id9 = simpleByteArray != null ? __ID_simpleByteArray : 0;
97100
Map stringObjectMap = entity.getStringObjectMap();
98101
int __id15 = stringObjectMap != null ? __ID_stringObjectMap : 0;
102+
Object flexProperty = entity.getFlexProperty();
103+
int __id16 = flexProperty != null ? __ID_flexProperty : 0;
99104

100105
collect430000(cursor, 0, 0,
101106
__id8, simpleString, 0, null,
102107
0, null, 0, null,
103108
__id9, simpleByteArray, __id15, __id15 != 0 ? stringObjectMapConverter.convertToDatabaseValue(stringObjectMap) : null,
104-
0, null);
109+
__id16, __id16 != 0 ? flexPropertyConverter.convertToDatabaseValue(flexProperty) : null);
105110

106111
collect313311(cursor, 0, 0,
107112
0, null, 0, null,

0 commit comments

Comments
 (0)