Skip to content

Commit cbbf9f2

Browse files
Merge branch 'dev' into sync
2 parents 5ef35b2 + bcc0a43 commit cbbf9f2

File tree

7 files changed

+261
-44
lines changed

7 files changed

+261
-44
lines changed

objectbox-java/src/main/java/io/objectbox/BoxStore.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package io.objectbox;
1818

19-
import io.objectbox.annotation.apihint.Beta;
2019
import org.greenrobot.essentials.collections.LongHashMap;
2120

2221
import java.io.Closeable;
@@ -42,6 +41,7 @@
4241
import javax.annotation.Nullable;
4342
import javax.annotation.concurrent.ThreadSafe;
4443

44+
import io.objectbox.annotation.apihint.Beta;
4545
import io.objectbox.annotation.apihint.Experimental;
4646
import io.objectbox.annotation.apihint.Internal;
4747
import io.objectbox.converter.PropertyConverter;
@@ -167,7 +167,7 @@ static native void nativeRegisterCustomType(long store, int entityId, int proper
167167

168168
static native int nativeCleanStaleReadTransactions(long store);
169169

170-
static native void nativeSetDbExceptionListener(long store, DbExceptionListener dbExceptionListener);
170+
static native void nativeSetDbExceptionListener(long store, @Nullable DbExceptionListener dbExceptionListener);
171171

172172
static native void nativeSetDebugFlags(long store, int debugFlags);
173173

@@ -255,6 +255,8 @@ public static boolean isSyncServerAvailable() {
255255

256256
try {
257257
handle = nativeCreateWithFlatOptions(builder.buildFlatStoreOptions(canonicalPath), builder.model);
258+
if(handle == 0) throw new DbException("Could not create native store");
259+
258260
int debugFlags = builder.debugFlags;
259261
if (debugFlags != 0) {
260262
debugTxRead = (debugFlags & DebugFlags.LOG_TRANSACTIONS_READ) != 0;
@@ -454,6 +456,8 @@ public Transaction beginTx() {
454456
System.out.println("Begin TX with commit count " + initialCommitCount);
455457
}
456458
long nativeTx = nativeBeginTx(handle);
459+
if(nativeTx == 0) throw new DbException("Could not create native transaction");
460+
457461
Transaction tx = new Transaction(this, nativeTx, initialCommitCount);
458462
synchronized (transactions) {
459463
transactions.add(tx);
@@ -478,6 +482,8 @@ public Transaction beginReadTx() {
478482
System.out.println("Begin read TX with commit count " + initialCommitCount);
479483
}
480484
long nativeTx = nativeBeginReadTx(handle);
485+
if(nativeTx == 0) throw new DbException("Could not create native read transaction");
486+
481487
Transaction tx = new Transaction(this, nativeTx, initialCommitCount);
482488
synchronized (transactions) {
483489
transactions.add(tx);
@@ -1087,10 +1093,12 @@ private void verifyObjectBrowserNotRunning() {
10871093
}
10881094

10891095
/**
1090-
* The given listener will be called when an exception is thrown.
1091-
* This for example allows a central error handling, e.g. a special logging for DB related exceptions.
1096+
* Sets a listener that will be called when an exception is thrown. Replaces a previously set listener.
1097+
* Set to {@code null} to remove the listener.
1098+
* <p>
1099+
* This for example allows central error handling or special logging for database-related exceptions.
10921100
*/
1093-
public void setDbExceptionListener(DbExceptionListener dbExceptionListener) {
1101+
public void setDbExceptionListener(@Nullable DbExceptionListener dbExceptionListener) {
10941102
nativeSetDbExceptionListener(handle, dbExceptionListener);
10951103
}
10961104

objectbox-java/src/main/java/io/objectbox/Transaction.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import io.objectbox.annotation.apihint.Experimental;
2424
import io.objectbox.annotation.apihint.Internal;
25+
import io.objectbox.exception.DbException;
2526
import io.objectbox.internal.CursorFactory;
2627

2728
@Internal
@@ -183,6 +184,7 @@ public <T> Cursor<T> createCursor(Class<T> entityClass) {
183184
EntityInfo<T> entityInfo = store.getEntityInfo(entityClass);
184185
CursorFactory<T> factory = entityInfo.getCursorFactory();
185186
long cursorHandle = nativeCreateCursor(transaction, entityInfo.getDbName(), entityClass);
187+
if(cursorHandle == 0) throw new DbException("Could not create native cursor");
186188
return factory.createCursor(this, cursorHandle, store);
187189
}
188190

objectbox-java/src/main/java/io/objectbox/internal/DebugCursor.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.objectbox.InternalAccess;
2222
import io.objectbox.Transaction;
2323
import io.objectbox.annotation.apihint.Beta;
24+
import io.objectbox.exception.DbException;
2425

2526
/** Not intended for normal use. */
2627
@Beta
@@ -40,7 +41,9 @@ public class DebugCursor implements Closeable {
4041

4142
public static DebugCursor create(Transaction tx) {
4243
long txHandle = InternalAccess.getHandle(tx);
43-
return new DebugCursor(tx, nativeCreate(txHandle));
44+
long handle = nativeCreate(txHandle);
45+
if(handle == 0) throw new DbException("Could not create native debug cursor");
46+
return new DebugCursor(tx, handle);
4447
}
4548

4649
public DebugCursor(Transaction tx, long handle) {

objectbox-java/src/main/java/io/objectbox/model/EntityFlags.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,13 @@ private EntityFlags() { }
2727
* Use the default (no arguments) constructor to create entities
2828
*/
2929
public static final int USE_NO_ARG_CONSTRUCTOR = 1;
30+
/**
31+
* Enable "data synchronization" for this entity type: objects will be synced with other stores over the network.
32+
* It's possible to have local-only (non-synced) types and synced types in the same store (schema/data model).
33+
*/
34+
public static final int SYNC_ENABLED = 2;
3035

31-
public static final String[] names = { "USE_NO_ARG_CONSTRUCTOR", };
36+
public static final String[] names = { "USE_NO_ARG_CONSTRUCTOR", "SYNC_ENABLED", };
3237

3338
public static String name(int e) { return names[e - USE_NO_ARG_CONSTRUCTOR]; }
3439
}

objectbox-java/src/main/java/io/objectbox/query/QueryBuilder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import io.objectbox.Property;
3030
import io.objectbox.annotation.apihint.Experimental;
3131
import io.objectbox.annotation.apihint.Internal;
32+
import io.objectbox.exception.DbException;
3233
import io.objectbox.relation.RelationInfo;
3334

3435
/**
@@ -191,6 +192,7 @@ public QueryBuilder(Box<T> box, long storeHandle, String entityName) {
191192
this.box = box;
192193
this.storeHandle = storeHandle;
193194
handle = nativeCreate(storeHandle, entityName);
195+
if(handle == 0) throw new DbException("Could not create native query builder");
194196
isSubQuery = false;
195197
}
196198

@@ -232,6 +234,7 @@ public Query<T> build() {
232234
throw new IllegalStateException("Incomplete logic condition. Use or()/and() between two conditions only.");
233235
}
234236
long queryHandle = nativeBuild(handle);
237+
if(queryHandle == 0) throw new DbException("Could not create native query");
235238
Query<T> query = new Query<>(box, queryHandle, eagerRelations, filter, comparator);
236239
close();
237240
return query;

tests/objectbox-java-test/src/test/java/io/objectbox/exception/ExceptionTest.java

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,167 @@
1616

1717
package io.objectbox.exception;
1818

19-
import io.objectbox.AbstractObjectBoxTest;
2019
import org.junit.Test;
2120

21+
import java.lang.ref.WeakReference;
2222
import java.util.ArrayList;
2323
import java.util.List;
24+
import java.util.concurrent.atomic.AtomicBoolean;
25+
import java.util.concurrent.atomic.AtomicInteger;
26+
27+
import io.objectbox.AbstractObjectBoxTest;
28+
2429

2530
import static org.junit.Assert.assertEquals;
31+
import static org.junit.Assert.assertFalse;
32+
import static org.junit.Assert.assertNotNull;
33+
import static org.junit.Assert.assertThrows;
34+
import static org.junit.Assert.assertTrue;
2635

36+
/**
37+
* Tests related to {@link DbExceptionListener}.
38+
*
39+
* Note: this test has an equivalent in test-java-android integration tests.
40+
*/
2741
public class ExceptionTest extends AbstractObjectBoxTest {
2842

43+
@Test
44+
public void exceptionListener_null_works() {
45+
store.setDbExceptionListener(null);
46+
}
47+
48+
@Test
49+
public void exceptionListener_closedStore_works() {
50+
store.close();
51+
store.setDbExceptionListener(e -> System.out.println("This is never called"));
52+
}
53+
54+
private static AtomicInteger weakRefListenerCalled = new AtomicInteger(0);
55+
56+
@Test
57+
public void exceptionListener_noLocalRef_works() throws InterruptedException {
58+
weakRefListenerCalled.set(0);
59+
// Note: do not use lambda, it would keep a reference to this class
60+
// and prevent garbage collection of the listener.
61+
//noinspection Convert2Lambda
62+
DbExceptionListener listenerNoRef = new DbExceptionListener() {
63+
@Override
64+
public void onDbException(Exception e) {
65+
System.out.println("Listener without strong reference is called");
66+
weakRefListenerCalled.incrementAndGet();
67+
}
68+
};
69+
WeakReference<DbExceptionListener> weakReference = new WeakReference<>(listenerNoRef);
70+
71+
// Set and clear local reference.
72+
store.setDbExceptionListener(listenerNoRef);
73+
//noinspection UnusedAssignment
74+
listenerNoRef = null;
75+
76+
// Ensure weak reference is kept as JNI is holding on to listener using a "global ref".
77+
int triesClearWeakRef = 5;
78+
while (weakReference.get() != null) {
79+
if (--triesClearWeakRef == 0) break;
80+
System.out.println("Suggesting GC");
81+
System.gc();
82+
System.runFinalization();
83+
//noinspection BusyWait
84+
Thread.sleep(300);
85+
}
86+
assertEquals("Failed to keep weak reference to listener", 0, triesClearWeakRef);
87+
assertNotNull(weakReference.get());
88+
89+
// Throw, listener should be called.
90+
assertThrows(
91+
DbException.class,
92+
() -> DbExceptionListenerJni.nativeThrowException(store.getNativeStore(), 0)
93+
);
94+
assertEquals(1, weakRefListenerCalled.get());
95+
96+
// Remove reference from native side.
97+
store.setDbExceptionListener(null);
98+
99+
// Try and succeed to release weak reference.
100+
triesClearWeakRef = 5;
101+
while (weakReference.get() != null) {
102+
if (--triesClearWeakRef == 0) break;
103+
System.out.println("Suggesting GC");
104+
System.gc();
105+
System.runFinalization();
106+
//noinspection BusyWait
107+
Thread.sleep(300);
108+
}
109+
assertTrue("Failed to release weak reference to listener", triesClearWeakRef > 0);
110+
111+
// Throw, listener should not be called.
112+
assertThrows(
113+
DbException.class,
114+
() -> DbExceptionListenerJni.nativeThrowException(store.getNativeStore(), 0)
115+
);
116+
assertEquals(1, weakRefListenerCalled.get());
117+
}
118+
119+
@Test
120+
public void exceptionListener_noref() throws InterruptedException {
121+
weakRefListenerCalled.set(0);
122+
123+
//noinspection Convert2Lambda
124+
store.setDbExceptionListener(new DbExceptionListener() {
125+
@Override
126+
public void onDbException(Exception e) {
127+
System.out.println("Listener without reference is called");
128+
weakRefListenerCalled.incrementAndGet();
129+
}
130+
});
131+
132+
for (int i = 0; i < 5; i++) {
133+
System.gc();
134+
System.runFinalization();
135+
Thread.sleep(100);
136+
}
137+
138+
// Throw, listener should be called.
139+
assertThrows(
140+
DbException.class,
141+
() -> DbExceptionListenerJni.nativeThrowException(store.getNativeStore(), 0)
142+
);
143+
assertEquals(1, weakRefListenerCalled.get());
144+
}
145+
146+
@Test
147+
public void exceptionListener_removing_works() {
148+
AtomicBoolean replacedListenerCalled = new AtomicBoolean(false);
149+
DbExceptionListener listenerRemoved = e -> replacedListenerCalled.set(true);
150+
151+
store.setDbExceptionListener(listenerRemoved);
152+
store.setDbExceptionListener(null);
153+
154+
assertThrows(
155+
DbException.class,
156+
() -> DbExceptionListenerJni.nativeThrowException(store.getNativeStore(), 0)
157+
);
158+
assertFalse("Should not have called removed DbExceptionListener.", replacedListenerCalled.get());
159+
}
160+
161+
@Test
162+
public void exceptionListener_replacing_works() {
163+
AtomicBoolean replacedListenerCalled = new AtomicBoolean(false);
164+
DbExceptionListener listenerReplaced = e -> replacedListenerCalled.set(true);
165+
166+
AtomicBoolean newListenerCalled = new AtomicBoolean(false);
167+
DbExceptionListener listenerNew = e -> newListenerCalled.set(true);
168+
169+
store.setDbExceptionListener(listenerReplaced);
170+
store.setDbExceptionListener(listenerNew);
171+
172+
assertThrows(
173+
DbException.class,
174+
() -> DbExceptionListenerJni.nativeThrowException(store.getNativeStore(), 0)
175+
);
176+
assertFalse("Should not have called replaced DbExceptionListener.", replacedListenerCalled.get());
177+
assertTrue("Failed to call new DbExceptionListener.", newListenerCalled.get());
178+
}
179+
29180
@Test
30181
public void testThrowExceptions() {
31182
final List<Exception> exs = new ArrayList<>();

0 commit comments

Comments
 (0)