From 955bd13baf9512bf62101844fed183232f64d75d Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 24 Feb 2020 14:03:00 +0100 Subject: [PATCH 01/34] Test observer is notified in order of publish request. --- .../io/objectbox/query/QueryObserverTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryObserverTest.java b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryObserverTest.java index 7e7d756f..c1154866 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryObserverTest.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryObserverTest.java @@ -23,11 +23,13 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import io.objectbox.AbstractObjectBoxTest; import io.objectbox.Box; import io.objectbox.TestEntity; import io.objectbox.reactive.DataObserver; +import io.objectbox.reactive.DataSubscription; import io.objectbox.reactive.DataTransformer; @@ -65,6 +67,43 @@ public void testObserver() { assertEquals(3, receivedChanges.get(0).size()); } + @Test + public void observer_resultsDeliveredInOrder() { + Query query = box.query().build(); + + final CountDownLatch latch = new CountDownLatch(2); + final AtomicBoolean isLongTransform = new AtomicBoolean(true); + final List placing = new CopyOnWriteArrayList<>(); + + // Block first onData call long enough so second one can race it. + DataSubscription subscription = query.subscribe().observer(data -> { + if (isLongTransform.compareAndSet(true, false)) { + // Wait long enough so publish triggered by transaction + // can overtake publish triggered during observer() call. + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + placing.add(1); // First, during observer() call. + } else { + placing.add(2); // Second, due to transaction. + } + latch.countDown(); + }); + + // Trigger publish due to transaction. + store.runInTx(() -> putTestEntities(1)); + + assertLatchCountedDown(latch, 3); + subscription.cancel(); + + // Second publish request should still deliver second. + assertEquals(2, placing.size()); + assertEquals(1, (int) placing.get(0)); + assertEquals(2, (int) placing.get(1)); + } + @Test public void testSingle() throws InterruptedException { putTestEntitiesScalars(); From ddde05c4440eeee4eab1f499779667e6fe69a6da Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 24 Feb 2020 10:01:38 +0100 Subject: [PATCH 02/34] QueryPublisher: queue publish requests to ensure delivery order matches. --- .../io/objectbox/query/QueryPublisher.java | 80 ++++++++++++++++--- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/objectbox-java/src/main/java/io/objectbox/query/QueryPublisher.java b/objectbox-java/src/main/java/io/objectbox/query/QueryPublisher.java index f71420af..c523042d 100644 --- a/objectbox-java/src/main/java/io/objectbox/query/QueryPublisher.java +++ b/objectbox-java/src/main/java/io/objectbox/query/QueryPublisher.java @@ -16,6 +16,8 @@ package io.objectbox.query; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @@ -29,13 +31,31 @@ import io.objectbox.reactive.DataPublisher; import io.objectbox.reactive.DataPublisherUtils; import io.objectbox.reactive.DataSubscription; +import io.objectbox.reactive.SubscriptionBuilder; +/** + * A {@link DataPublisher} that subscribes to an ObjectClassPublisher if there is at least one observer. + * Publishing is requested if the ObjectClassPublisher reports changes, a subscription is + * {@link SubscriptionBuilder#observer(DataObserver) observed} or {@link Query#publish()} is called. + * For publishing the query is re-run and the result delivered to the current observers. + * Results are published on a single thread, one at a time, in the order publishing was requested. + */ @Internal -class QueryPublisher implements DataPublisher> { +class QueryPublisher implements DataPublisher>, Runnable { private final Query query; private final Box box; private final Set>> observers = new CopyOnWriteArraySet<>(); + private final Deque>> publishQueue = new ArrayDeque<>(); + private volatile boolean publisherRunning = false; + + private static class AllObservers implements DataObserver> { + @Override + public void onData(List data) { + } + } + /** Placeholder observer if all observers should be notified. */ + private final AllObservers ALL_OBSERVERS = new AllObservers<>(); private DataObserver> objectClassObserver; private DataSubscription objectClassSubscription; @@ -76,26 +96,60 @@ public void onData(Class objectClass) { } @Override - public void publishSingle(final DataObserver> observer, @Nullable Object param) { - box.getStore().internalScheduleThread(new Runnable() { - @Override - public void run() { - List result = query.find(); - observer.onData(result); + public void publishSingle(DataObserver> observer, @Nullable Object param) { + synchronized (publishQueue) { + publishQueue.add(observer); + if (!publisherRunning) { + publisherRunning = true; + box.getStore().internalScheduleThread(this); } - }); + } } void publish() { - box.getStore().internalScheduleThread(new Runnable() { - @Override - public void run() { + synchronized (publishQueue) { + publishQueue.add(ALL_OBSERVERS); + if (!publisherRunning) { + publisherRunning = true; + box.getStore().internalScheduleThread(this); + } + } + } + + @Override + public void run() { + /* + * Process publish requests for this query on a single thread to avoid an older request + * racing a new one (and causing outdated results to be delivered last). + */ + try { + while (true) { + // Get next observer(s). + DataObserver> observer; + synchronized (publishQueue) { + observer = publishQueue.pollFirst(); + if (observer == null) { + publisherRunning = false; + break; + } + } + + // Query, then notify observer(s). List result = query.find(); - for (DataObserver> observer : observers) { + if (ALL_OBSERVERS.equals(observer)) { + // Use current list of observers to avoid notifying unsubscribed observers. + Set>> observers = this.observers; + for (DataObserver> dataObserver : observers) { + dataObserver.onData(result); + } + } else { observer.onData(result); } } - }); + } finally { + // Re-set if wrapped code throws, otherwise this publisher can no longer publish. + publisherRunning = false; + } } @Override From e845fe4c52abfd10f354248a4e8b974a3e5e07b8 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 24 Feb 2020 13:10:19 +0100 Subject: [PATCH 03/34] Test transformed data is received in order of publish. --- .../io/objectbox/ObjectClassObserverTest.java | 38 ++++++++++++++++++ .../io/objectbox/query/QueryObserverTest.java | 39 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/ObjectClassObserverTest.java b/tests/objectbox-java-test/src/test/java/io/objectbox/ObjectClassObserverTest.java index 936daefa..37ce72e1 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/ObjectClassObserverTest.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/ObjectClassObserverTest.java @@ -23,8 +23,10 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import io.objectbox.query.QueryObserverTest; import io.objectbox.reactive.DataObserver; import io.objectbox.reactive.DataSubscription; import io.objectbox.reactive.DataTransformer; @@ -194,6 +196,42 @@ private void testTransform(TestScheduler scheduler) throws InterruptedException assertEquals(0, objectCounts.size()); } + /** + * There is an identical test asserting QueryPublisher at + * {@link QueryObserverTest#transform_inOrderOfPublish()}. + */ + @Test + public void transform_inOrderOfPublish() { + final CountDownLatch latch = new CountDownLatch(2); + final AtomicBoolean isLongTransform = new AtomicBoolean(true); + final List placing = new CopyOnWriteArrayList<>(); + + // Make first transformation take longer than second. + DataSubscription subscription = store.subscribe(TestEntity.class).transform(source -> { + if (isLongTransform.compareAndSet(true, false)) { + // Wait long enough so publish triggered by transaction + // can overtake publish triggered during observer() call. + Thread.sleep(1000); + return 1; // First, during observer() call. + } + return 2; // Second, due to transaction. + }).observer(data -> { + placing.add(data); + latch.countDown(); + }); + + // Trigger publish due to transaction. + store.runInTx(() -> putTestEntities(1)); + + assertLatchCountedDown(latch, 3); + subscription.cancel(); + + // Second publish request should still deliver second. + assertEquals(2, placing.size()); + assertEquals(1, (int) placing.get(0)); + assertEquals(2, (int) placing.get(1)); + } + @Test public void testScheduler() { TestScheduler scheduler = new TestScheduler(); diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryObserverTest.java b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryObserverTest.java index c1154866..a6d100f0 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryObserverTest.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryObserverTest.java @@ -27,6 +27,7 @@ import io.objectbox.AbstractObjectBoxTest; import io.objectbox.Box; +import io.objectbox.ObjectClassObserverTest; import io.objectbox.TestEntity; import io.objectbox.reactive.DataObserver; import io.objectbox.reactive.DataSubscription; @@ -149,6 +150,44 @@ public void testTransformer() throws InterruptedException { assertEquals(2003 + 2007 + 2002, (int) receivedSums.get(1)); } + /** + * There is an identical test asserting ObjectClassPublisher at + * {@link ObjectClassObserverTest#transform_inOrderOfPublish()}. + */ + @Test + public void transform_inOrderOfPublish() { + Query query = box.query().build(); + + final CountDownLatch latch = new CountDownLatch(2); + final AtomicBoolean isLongTransform = new AtomicBoolean(true); + final List placing = new CopyOnWriteArrayList<>(); + + // Make first transformation take longer than second. + DataSubscription subscription = query.subscribe().transform(source -> { + if (isLongTransform.compareAndSet(true, false)) { + // Wait long enough so publish triggered by transaction + // can overtake publish triggered during observer() call. + Thread.sleep(1000); + return 1; // First, during observer() call. + } + return 2; // Second, due to transaction. + }).observer(data -> { + placing.add(data); + latch.countDown(); + }); + + // Trigger publish due to transaction. + store.runInTx(() -> putTestEntities(1)); + + assertLatchCountedDown(latch, 3); + subscription.cancel(); + + // Second publish request should still deliver second. + assertEquals(2, placing.size()); + assertEquals(1, (int) placing.get(0)); + assertEquals(2, (int) placing.get(1)); + } + private void putTestEntitiesScalars() { putTestEntities(10, null, 2000); } From 18f9be5a6d45e4b66c05cbf0a807ccbb51db7991 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 24 Feb 2020 13:16:32 +0100 Subject: [PATCH 04/34] To deliver in order run transform on same thread as ActionObserver was notified on. --- .../src/main/java/io/objectbox/BoxStore.java | 4 +- .../main/java/io/objectbox/query/Query.java | 2 +- .../reactive/SubscriptionBuilder.java | 42 +++++++++---------- .../java/io/objectbox/query/MockQuery.java | 2 +- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/objectbox-java/src/main/java/io/objectbox/BoxStore.java b/objectbox-java/src/main/java/io/objectbox/BoxStore.java index f75e3f8e..69ce6b8a 100644 --- a/objectbox-java/src/main/java/io/objectbox/BoxStore.java +++ b/objectbox-java/src/main/java/io/objectbox/BoxStore.java @@ -918,7 +918,7 @@ long internalHandle() { * Note that failed or aborted transaction do not trigger observers. */ public SubscriptionBuilder subscribe() { - return new SubscriptionBuilder<>(objectClassPublisher, null, threadPool); + return new SubscriptionBuilder<>(objectClassPublisher, null); } @Experimental @@ -977,7 +977,7 @@ public void setDbExceptionListener(DbExceptionListener dbExceptionListener) { */ @SuppressWarnings("unchecked") public SubscriptionBuilder> subscribe(Class forClass) { - return new SubscriptionBuilder<>((DataPublisher) objectClassPublisher, forClass, threadPool); + return new SubscriptionBuilder<>((DataPublisher) objectClassPublisher, forClass); } @Internal diff --git a/objectbox-java/src/main/java/io/objectbox/query/Query.java b/objectbox-java/src/main/java/io/objectbox/query/Query.java index 91eec7f9..09501805 100644 --- a/objectbox-java/src/main/java/io/objectbox/query/Query.java +++ b/objectbox-java/src/main/java/io/objectbox/query/Query.java @@ -634,7 +634,7 @@ public Long call(long cursorHandle) { * it may be GCed and observers may become stale (won't receive anymore data). */ public SubscriptionBuilder> subscribe() { - return new SubscriptionBuilder<>(publisher, null, box.getStore().internalThreadPool()); + return new SubscriptionBuilder<>(publisher, null); } /** diff --git a/objectbox-java/src/main/java/io/objectbox/reactive/SubscriptionBuilder.java b/objectbox-java/src/main/java/io/objectbox/reactive/SubscriptionBuilder.java index cf3f9f79..dddb897b 100644 --- a/objectbox-java/src/main/java/io/objectbox/reactive/SubscriptionBuilder.java +++ b/objectbox-java/src/main/java/io/objectbox/reactive/SubscriptionBuilder.java @@ -16,11 +16,8 @@ package io.objectbox.reactive; -import java.util.concurrent.ExecutorService; - import javax.annotation.Nullable; -import io.objectbox.annotation.apihint.Beta; import io.objectbox.annotation.apihint.Internal; /** @@ -45,7 +42,6 @@ public class SubscriptionBuilder { private final DataPublisher publisher; private final Object publisherParam; - private final ExecutorService threadPool; private DataObserver observer; // private Runnable firstRunnable; private boolean weak; @@ -59,10 +55,9 @@ public class SubscriptionBuilder { @Internal - public SubscriptionBuilder(DataPublisher publisher, @Nullable Object param, ExecutorService threadPool) { + public SubscriptionBuilder(DataPublisher publisher, @Nullable Object param) { this.publisher = publisher; publisherParam = param; - this.threadPool = threadPool; } // public Observable runFirst(Runnable firstRunnable) { @@ -215,22 +210,27 @@ public void onData(final T data) { } } + /** + * Runs on the thread of the {@link #onData(Object)} caller to ensure data is delivered + * in the same order as {@link #onData(Object)} was called, to prevent delivering stale data. + *

+ * For both ObjectClassPublisher and QueryPublisher this is the asynchronous + * thread publish requests are processed on. + *

+ * This could be optimized in the future to allow parallel execution, + * but this would require an ordering mechanism for the transformed data. + */ private void transformAndContinue(final T data) { - threadPool.submit(new Runnable() { - @Override - public void run() { - if (subscription.isCanceled()) { - return; - } - try { - // Type erasure FTW - T result = (T) transformer.transform(data); - callOnData(result); - } catch (Throwable th) { - callOnError(th, "Transformer failed without an ErrorObserver set"); - } - } - }); + if (subscription.isCanceled()) { + return; + } + try { + // Type erasure FTW + T result = (T) transformer.transform(data); + callOnData(result); + } catch (Throwable th) { + callOnError(th, "Transformer failed without an ErrorObserver set"); + } } private void callOnError(Throwable th, String msgNoErrorObserver) { diff --git a/objectbox-rxjava/src/test/java/io/objectbox/query/MockQuery.java b/objectbox-rxjava/src/test/java/io/objectbox/query/MockQuery.java index 3e86e7c7..55958a9b 100644 --- a/objectbox-rxjava/src/test/java/io/objectbox/query/MockQuery.java +++ b/objectbox-rxjava/src/test/java/io/objectbox/query/MockQuery.java @@ -35,7 +35,7 @@ public MockQuery(boolean hasOrder) { // when(box.getStore()).thenReturn(boxStore); query = mock(Query.class); fakeQueryPublisher = new FakeQueryPublisher(); - SubscriptionBuilder subscriptionBuilder = new SubscriptionBuilder(fakeQueryPublisher, null, null); + SubscriptionBuilder subscriptionBuilder = new SubscriptionBuilder(fakeQueryPublisher, null); when(query.subscribe()).thenReturn(subscriptionBuilder); } From 4d7c147395e31a3d7835e4ebaaeca2cca860e3e8 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 24 Feb 2020 13:37:22 +0100 Subject: [PATCH 05/34] ObjectClassPublisher: process all requests on same thread to ensure order. --- .../io/objectbox/ObjectClassPublisher.java | 87 ++++++++++++------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/objectbox-java/src/main/java/io/objectbox/ObjectClassPublisher.java b/objectbox-java/src/main/java/io/objectbox/ObjectClassPublisher.java index 25ad301d..fbf21e3b 100644 --- a/objectbox-java/src/main/java/io/objectbox/ObjectClassPublisher.java +++ b/objectbox-java/src/main/java/io/objectbox/ObjectClassPublisher.java @@ -31,12 +31,28 @@ import io.objectbox.reactive.DataObserver; import io.objectbox.reactive.DataPublisher; import io.objectbox.reactive.DataPublisherUtils; +import io.objectbox.reactive.SubscriptionBuilder; +/** + * A {@link DataPublisher} that notifies {@link DataObserver}s about changes in an entity box. + * Publishing is requested when a subscription is {@link SubscriptionBuilder#observer(DataObserver) observed} and + * then by {@link BoxStore} for each {@link BoxStore#txCommitted(Transaction, int[]) txCommitted}. + * Publish requests are processed on a single thread, one at a time, in the order publishing was requested. + */ +@SuppressWarnings("rawtypes") @Internal class ObjectClassPublisher implements DataPublisher, Runnable { final BoxStore boxStore; final MultimapSet> observersByEntityTypeId = MultimapSet.create(SetType.THREAD_SAFE); - final Deque changesQueue = new ArrayDeque<>(); + private final Deque changesQueue = new ArrayDeque<>(); + private static class PublishRequest { + @Nullable private final DataObserver observer; + private final int[] entityTypeIds; + PublishRequest(@Nullable DataObserver observer, int[] entityTypeIds) { + this.observer = observer; + this.entityTypeIds = entityTypeIds; + } + } volatile boolean changePublisherRunning; ObjectClassPublisher(BoxStore boxStore) { @@ -76,21 +92,19 @@ private void unsubscribe(DataObserver observer, int entityTypeId) { } @Override - public void publishSingle(final DataObserver observer, @Nullable final Object forClass) { - boxStore.internalScheduleThread(new Runnable() { - @Override - public void run() { - Collection entityClasses = forClass != null ? Collections.singletonList((Class) forClass) : - boxStore.getAllEntityClasses(); - for (Class entityClass : entityClasses) { - try { - observer.onData(entityClass); - } catch (RuntimeException e) { - handleObserverException(entityClass); - } - } + public void publishSingle(DataObserver observer, @Nullable Object forClass) { + int[] entityTypeIds = forClass != null + ? new int[]{boxStore.getEntityTypeIdOrThrow((Class) forClass)} + : boxStore.getAllEntityTypeIds(); + + synchronized (changesQueue) { + changesQueue.add(new PublishRequest(observer, entityTypeIds)); + // Only one thread at a time. + if (!changePublisherRunning) { + changePublisherRunning = true; + boxStore.internalScheduleThread(this); } - }); + } } private void handleObserverException(Class objectClass) { @@ -107,8 +121,8 @@ private void handleObserverException(Class objectClass) { */ void publish(int[] entityTypeIdsAffected) { synchronized (changesQueue) { - changesQueue.add(entityTypeIdsAffected); - // Only one thread at a time + changesQueue.add(new PublishRequest(null, entityTypeIdsAffected)); + // Only one thread at a time. if (!changePublisherRunning) { changePublisherRunning = true; boxStore.internalScheduleThread(this); @@ -116,30 +130,41 @@ void publish(int[] entityTypeIdsAffected) { } } + /** + * Processes publish requests using a single thread to prevent any data generated by observers to get stale. + * This publisher on its own can NOT deliver stale data (the entity class types do not change). + * However, a {@link DataObserver} of this publisher might apply a {@link io.objectbox.reactive.DataTransformer} + * which queries for data which CAN get stale if delivered out of order. + */ @Override public void run() { try { while (true) { - // We do not join all available array, just in case the app relies on a specific order - int[] entityTypeIdsAffected; + PublishRequest request; synchronized (changesQueue) { - entityTypeIdsAffected = changesQueue.pollFirst(); - if (entityTypeIdsAffected == null) { + request = changesQueue.pollFirst(); + if (request == null) { changePublisherRunning = false; break; } } - for (int entityTypeId : entityTypeIdsAffected) { - Collection> observers = observersByEntityTypeId.get(entityTypeId); - if (observers != null && !observers.isEmpty()) { - Class objectClass = boxStore.getEntityClassOrThrow(entityTypeId); - try { - for (DataObserver observer : observers) { - observer.onData(objectClass); - } - } catch (RuntimeException e) { - handleObserverException(objectClass); + + for (int entityTypeId : request.entityTypeIds) { + // If no specific observer specified, notify all current observers. + Collection> observers = request.observer != null + ? Collections.singletonList(request.observer) + : observersByEntityTypeId.get(entityTypeId); + if (observers == null || observers.isEmpty()) { + continue; // No observers for this entity type. + } + + Class entityClass = boxStore.getEntityClassOrThrow(entityTypeId); + try { + for (DataObserver observer : observers) { + observer.onData(entityClass); } + } catch (RuntimeException e) { + handleObserverException(entityClass); } } } From 24a5e82cfd3552e50687ddc70d1c8821070ccdf7 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 2 Mar 2020 14:53:21 +0100 Subject: [PATCH 06/34] QueryPublisher: notify all queued observers to reduce wait. --- .../io/objectbox/query/QueryPublisher.java | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/objectbox-java/src/main/java/io/objectbox/query/QueryPublisher.java b/objectbox-java/src/main/java/io/objectbox/query/QueryPublisher.java index c523042d..73fd3267 100644 --- a/objectbox-java/src/main/java/io/objectbox/query/QueryPublisher.java +++ b/objectbox-java/src/main/java/io/objectbox/query/QueryPublisher.java @@ -17,6 +17,7 @@ package io.objectbox.query; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Deque; import java.util.List; import java.util.Set; @@ -49,13 +50,13 @@ class QueryPublisher implements DataPublisher>, Runnable { private final Deque>> publishQueue = new ArrayDeque<>(); private volatile boolean publisherRunning = false; - private static class AllObservers implements DataObserver> { + private static class SubscribedObservers implements DataObserver> { @Override public void onData(List data) { } } - /** Placeholder observer if all observers should be notified. */ - private final AllObservers ALL_OBSERVERS = new AllObservers<>(); + /** Placeholder observer if subscribed observers should be notified. */ + private final SubscribedObservers SUBSCRIBED_OBSERVERS = new SubscribedObservers<>(); private DataObserver> objectClassObserver; private DataSubscription objectClassSubscription; @@ -108,7 +109,7 @@ public void publishSingle(DataObserver> observer, @Nullable Object param void publish() { synchronized (publishQueue) { - publishQueue.add(ALL_OBSERVERS); + publishQueue.add(SUBSCRIBED_OBSERVERS); if (!publisherRunning) { publisherRunning = true; box.getStore().internalScheduleThread(this); @@ -116,34 +117,47 @@ void publish() { } } + /** + * Processes publish requests for this query on a single thread to prevent + * older query results getting delivered after newer query results. + * To speed up processing each loop publishes to all queued observers instead of just the next in line. + * This reduces time spent querying and waiting for DataObserver.onData() and their potential DataTransformers. + */ @Override public void run() { - /* - * Process publish requests for this query on a single thread to avoid an older request - * racing a new one (and causing outdated results to be delivered last). - */ try { while (true) { - // Get next observer(s). - DataObserver> observer; + // Get all queued observer(s), stop processing if none. + List>> singlePublishObservers = new ArrayList<>(); + boolean notifySubscribedObservers = false; synchronized (publishQueue) { - observer = publishQueue.pollFirst(); - if (observer == null) { + DataObserver> nextObserver; + while ((nextObserver = publishQueue.poll()) != null) { + if (SUBSCRIBED_OBSERVERS.equals(nextObserver)) { + notifySubscribedObservers = true; + } else { + singlePublishObservers.add(nextObserver); + } + } + if (!notifySubscribedObservers && singlePublishObservers.isEmpty()) { publisherRunning = false; - break; + break; // Stop. } } - // Query, then notify observer(s). + // Query. List result = query.find(); - if (ALL_OBSERVERS.equals(observer)) { + + // Notify observer(s). + for (DataObserver> observer : singlePublishObservers) { + observer.onData(result); + } + if (notifySubscribedObservers) { // Use current list of observers to avoid notifying unsubscribed observers. Set>> observers = this.observers; for (DataObserver> dataObserver : observers) { dataObserver.onData(result); } - } else { - observer.onData(result); } } } finally { From c99832eb4dce75117432aad7ccda871e313347a9 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 2 Mar 2020 16:20:35 +0100 Subject: [PATCH 07/34] Update subscriber and transformer documentation. --- .../objectbox/reactive/DataTransformer.java | 11 +++--- .../reactive/SubscriptionBuilder.java | 34 +++++++++++++------ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/objectbox-java/src/main/java/io/objectbox/reactive/DataTransformer.java b/objectbox-java/src/main/java/io/objectbox/reactive/DataTransformer.java index 35a3f9c8..d34f1cea 100644 --- a/objectbox-java/src/main/java/io/objectbox/reactive/DataTransformer.java +++ b/objectbox-java/src/main/java/io/objectbox/reactive/DataTransformer.java @@ -16,17 +16,16 @@ package io.objectbox.reactive; -import javax.annotation.Nullable; - /** * Transforms or processes data before it is given to subscribed {@link DataObserver}s. A transformer is set via * {@link SubscriptionBuilder#transform(DataTransformer)}. - * + *

* Note that a transformer is not required to actually "transform" any data. * Technically, it's fine to return the same data it received and just do some processing with it. - * - * Threading notes: Note that the transformer is always executed asynchronously. - * It is OK to perform long lasting operations. + *

+ * Threading notes: transformations are executed sequentially on a background thread + * owned by the subscription publisher. It is OK to perform long lasting operations, + * however this will block notifications to all other observers until finished. * * @param Data type this transformer receives * @param Type of transformed data diff --git a/objectbox-java/src/main/java/io/objectbox/reactive/SubscriptionBuilder.java b/objectbox-java/src/main/java/io/objectbox/reactive/SubscriptionBuilder.java index dddb897b..abedaa76 100644 --- a/objectbox-java/src/main/java/io/objectbox/reactive/SubscriptionBuilder.java +++ b/objectbox-java/src/main/java/io/objectbox/reactive/SubscriptionBuilder.java @@ -19,13 +19,14 @@ import javax.annotation.Nullable; import io.objectbox.annotation.apihint.Internal; +import io.objectbox.query.Query; /** * Builds a {@link DataSubscription} for a {@link DataObserver} passed via {@link #observer(DataObserver)}. * Note that the call to {@link #observer(DataObserver)} is mandatory to create the subscription - * if you forget it, nothing will happen. *

- * When subscribing to a data source such as {@link io.objectbox.query.Query}, this builder allows to configure: + * When subscribing to a data source such as {@link Query}, this builder allows to configure: *

    *
  • weakly referenced observer via {@link #weak()}
  • *
  • a data transform operation via {@link #transform(DataTransformer)}
  • @@ -78,11 +79,21 @@ public SubscriptionBuilder weak() { return this; } + /** + * Only deliver the latest data once, do not subscribe for data changes. + * + * @see #onlyChanges() + */ public SubscriptionBuilder single() { single = true; return this; } + /** + * Upon subscribing do not deliver the latest data, only once there are changes. + * + * @see #single() + */ public SubscriptionBuilder onlyChanges() { onlyChanges = true; return this; @@ -94,14 +105,12 @@ public SubscriptionBuilder onlyChanges() { // } /** - * Transforms the original data from the publisher to something that is more helpful to your application. - * The transformation is done in an asynchronous thread. - * The observer will be called in the same asynchronous thread unless a Scheduler is defined using - * {@link #on(Scheduler)}. + * Transforms the original data from the publisher to some other type. + * All transformations run sequentially in an asynchronous thread owned by the publisher. *

    - * This is roughly equivalent to the map operator as known in Rx and Kotlin. + * This is similar to the map operator of Rx and Kotlin. * - * @param The class the data is transformed to + * @param The type data is transformed to. */ public SubscriptionBuilder transform(final DataTransformer transformer) { if (this.transformer != null) { @@ -139,11 +148,16 @@ public SubscriptionBuilder on(Scheduler scheduler) { } /** - * The given observer is subscribed to the publisher. This method MUST be called to complete a subscription. + * Sets the observer for this subscription and requests the latest data to be delivered immediately + * and subscribes to the publisher for data updates, unless configured differently + * using {@link #single()} or {@link #onlyChanges()}. + *

    + * Results are delivered on a background thread owned by the publisher, + * unless a scheduler was set using {@link #on(Scheduler)}. *

    - * Note: you must keep the returned {@link DataSubscription} to cancel it. + * The returned {@link DataSubscription} must be canceled once the observer should no longer receive notifications. * - * @return an subscription object used for canceling further notifications to the observer + * @return A {@link DataSubscription} to cancel further notifications to the observer. */ public DataSubscription observer(DataObserver observer) { WeakDataObserver weakObserver = null; From 43a5ba8189a90bd4bc30a2e6b3b09c5776031666 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 3 Mar 2020 08:48:54 +0100 Subject: [PATCH 08/34] Add new QueryCondition interface, add Property condition methods. - Separate public QueryCondition API from implementation to not expose internal APIs. - PropertyQueryCondition interface to expose alias() method only for property conditions (not for and/or conditions). - Add new Box query method accepting QueryCondition. --- .../src/main/java/io/objectbox/Box.java | 26 +- .../src/main/java/io/objectbox/Property.java | 235 +++++++++-- .../objectbox/query/LogicQueryCondition.java | 58 +++ .../query/PropertyQueryCondition.java | 15 + .../query/PropertyQueryConditionImpl.java | 379 ++++++++++++++++++ .../java/io/objectbox/query/QueryBuilder.java | 38 ++ .../io/objectbox/query/QueryCondition.java | 279 +------------ .../objectbox/query/QueryConditionImpl.java | 22 + .../kotlin/io/objectbox/kotlin/Extensions.kt | 21 +- 9 files changed, 765 insertions(+), 308 deletions(-) create mode 100644 objectbox-java/src/main/java/io/objectbox/query/LogicQueryCondition.java create mode 100644 objectbox-java/src/main/java/io/objectbox/query/PropertyQueryCondition.java create mode 100644 objectbox-java/src/main/java/io/objectbox/query/PropertyQueryConditionImpl.java create mode 100644 objectbox-java/src/main/java/io/objectbox/query/QueryConditionImpl.java diff --git a/objectbox-java/src/main/java/io/objectbox/Box.java b/objectbox-java/src/main/java/io/objectbox/Box.java index c15ef3d9..0df68074 100644 --- a/objectbox-java/src/main/java/io/objectbox/Box.java +++ b/objectbox-java/src/main/java/io/objectbox/Box.java @@ -19,7 +19,6 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -37,6 +36,7 @@ import io.objectbox.internal.IdGetter; import io.objectbox.internal.ReflectionCache; import io.objectbox.query.QueryBuilder; +import io.objectbox.query.QueryCondition; import io.objectbox.relation.RelationInfo; /** @@ -555,6 +555,30 @@ public QueryBuilder query() { return new QueryBuilder<>(this, store.internalHandle(), store.getDbName(entityClass)); } + /** + * Applies the given query conditions and returns the builder for further customization, such as result order. + * Build the condition using the properties from your entity underscore classes. + *

    + * An example with a nested OR condition: + *

    +     * # Java
    +     * box.query(User_.name.equal("Jane")
    +     *         .and(User_.age.less(12)
    +     *                 .or(User_.status.equal("child"))));
    +     *
    +     * # Kotlin
    +     * box.query(User_.name.equal("Jane")
    +     *         and (User_.age.less(12)
    +     *         or User_.status.equal("child")))
    +     * 
    + * This method is a shortcut for {@code query().apply(condition)}. + * + * @see QueryBuilder#apply(QueryCondition) + */ + public QueryBuilder query(QueryCondition queryCondition) { + return query().apply(queryCondition); + } + public BoxStore getStore() { return store; } diff --git a/objectbox-java/src/main/java/io/objectbox/Property.java b/objectbox-java/src/main/java/io/objectbox/Property.java index 76801931..853b3ec0 100644 --- a/objectbox-java/src/main/java/io/objectbox/Property.java +++ b/objectbox-java/src/main/java/io/objectbox/Property.java @@ -17,16 +17,26 @@ package io.objectbox; import java.io.Serializable; -import java.util.Collection; +import java.util.Date; import javax.annotation.Nullable; import io.objectbox.annotation.apihint.Internal; import io.objectbox.converter.PropertyConverter; import io.objectbox.exception.DbException; -import io.objectbox.query.QueryCondition; -import io.objectbox.query.QueryCondition.PropertyCondition; -import io.objectbox.query.QueryCondition.PropertyCondition.Operation; +import io.objectbox.query.PropertyQueryCondition; +import io.objectbox.query.PropertyQueryConditionImpl.ByteArrayCondition; +import io.objectbox.query.PropertyQueryConditionImpl.DoubleCondition; +import io.objectbox.query.PropertyQueryConditionImpl.DoubleDoubleCondition; +import io.objectbox.query.PropertyQueryConditionImpl.IntArrayCondition; +import io.objectbox.query.PropertyQueryConditionImpl.LongArrayCondition; +import io.objectbox.query.PropertyQueryConditionImpl.LongCondition; +import io.objectbox.query.PropertyQueryConditionImpl.LongLongCondition; +import io.objectbox.query.PropertyQueryConditionImpl.NullCondition; +import io.objectbox.query.PropertyQueryConditionImpl.StringArrayCondition; +import io.objectbox.query.PropertyQueryConditionImpl.StringCondition; +import io.objectbox.query.PropertyQueryConditionImpl.StringCondition.Operation; +import io.objectbox.query.QueryBuilder.StringOrder; /** * Meta data describing a property of an ObjectBox entity. @@ -90,71 +100,208 @@ public Property(EntityInfo entity, int ordinal, int id, Class type, S this.customType = customType; } - /** Creates an "equal ('=')" condition for this property. */ - public QueryCondition eq(Object value) { - return new PropertyCondition(this, Operation.EQUALS, value); + /** Creates an "IS NULL" condition for this property. */ + public PropertyQueryCondition isNull() { + return new NullCondition<>(this, NullCondition.Operation.IS_NULL); } - /** Creates an "not equal ('<>')" condition for this property. */ - public QueryCondition notEq(Object value) { - return new PropertyCondition(this, Operation.NOT_EQUALS, value); + /** Creates an "IS NOT NULL" condition for this property. */ + public PropertyQueryCondition notNull() { + return new NullCondition<>(this, NullCondition.Operation.NOT_NULL); } - /** Creates an "BETWEEN ... AND ..." condition for this property. */ - public QueryCondition between(Object value1, Object value2) { - Object[] values = {value1, value2}; - return new PropertyCondition(this, Operation.BETWEEN, values); + /** Creates an "equal ('=')" condition for this property. */ + public PropertyQueryCondition equal(boolean value) { + return new LongCondition<>(this, LongCondition.Operation.EQUAL, value); } - /** Creates an "IN (..., ..., ...)" condition for this property. */ - public QueryCondition in(Object... inValues) { - return new PropertyCondition(this, Operation.IN, inValues); + /** Creates a "not equal ('<>')" condition for this property. */ + public PropertyQueryCondition notEqual(boolean value) { + return new LongCondition<>(this, LongCondition.Operation.NOT_EQUAL, value); } /** Creates an "IN (..., ..., ...)" condition for this property. */ - public QueryCondition in(Collection inValues) { - return in(inValues.toArray()); + public PropertyQueryCondition oneOf(int[] values) { + return new IntArrayCondition<>(this, IntArrayCondition.Operation.IN, values); } - /** Creates an "greater than ('>')" condition for this property. */ - public QueryCondition gt(Object value) { - return new PropertyCondition(this, Operation.GREATER_THAN, value); + /** Creates a "NOT IN (..., ..., ...)" condition for this property. */ + public PropertyQueryCondition notOneOf(int[] values) { + return new IntArrayCondition<>(this, IntArrayCondition.Operation.NOT_IN, values); } - /** Creates an "less than ('<')" condition for this property. */ - public QueryCondition lt(Object value) { - return new PropertyCondition(this, Operation.LESS_THAN, value); + /** Creates an "equal ('=')" condition for this property. */ + public PropertyQueryCondition equal(long value) { + return new LongCondition<>(this, LongCondition.Operation.EQUAL, value); } - /** Creates an "IS NULL" condition for this property. */ - public QueryCondition isNull() { - return new PropertyCondition(this, Operation.IS_NULL, null); + /** Creates a "not equal ('<>')" condition for this property. */ + public PropertyQueryCondition notEqual(long value) { + return new LongCondition<>(this, LongCondition.Operation.NOT_EQUAL, value); } - /** Creates an "IS NOT NULL" condition for this property. */ - public QueryCondition isNotNull() { - return new PropertyCondition(this, Operation.IS_NOT_NULL, null); + /** Creates a "greater than ('>')" condition for this property. */ + public PropertyQueryCondition greater(long value) { + return new LongCondition<>(this, LongCondition.Operation.GREATER, value); } - /** - * @see io.objectbox.query.QueryBuilder#contains(Property, String) - */ - public QueryCondition contains(String value) { - return new PropertyCondition(this, Operation.CONTAINS, value); + /** Creates a "less than ('<')" condition for this property. */ + public PropertyQueryCondition less(long value) { + return new LongCondition<>(this, LongCondition.Operation.LESS, value); } - /** - * @see io.objectbox.query.QueryBuilder#startsWith(Property, String) - */ - public QueryCondition startsWith(String value) { - return new PropertyCondition(this, Operation.STARTS_WITH, value); + /** Creates an "IN (..., ..., ...)" condition for this property. */ + public PropertyQueryCondition oneOf(long[] values) { + return new LongArrayCondition<>(this, LongArrayCondition.Operation.IN, values); + } + + /** Creates a "NOT IN (..., ..., ...)" condition for this property. */ + public PropertyQueryCondition notOneOf(long[] values) { + return new LongArrayCondition<>(this, LongArrayCondition.Operation.NOT_IN, values); + } + + /** Creates an "BETWEEN ... AND ..." condition for this property. */ + public PropertyQueryCondition between(long lowerBoundary, long upperBoundary) { + return new LongLongCondition<>(this, LongLongCondition.Operation.BETWEEN, lowerBoundary, upperBoundary); } /** - * @see io.objectbox.query.QueryBuilder#endsWith(Property, String) + * Calls {@link #between(double, double)} with {@code value - tolerance} as lower bound and + * {@code value + tolerance} as upper bound. */ - public QueryCondition endsWith(String value) { - return new PropertyCondition(this, Operation.ENDS_WITH, value); + public PropertyQueryCondition equal(double value, double tolerance) { + return new DoubleDoubleCondition<>(this, DoubleDoubleCondition.Operation.BETWEEN, + value - tolerance, value + tolerance); + } + + /** Creates a "greater than ('>')" condition for this property. */ + public PropertyQueryCondition greater(double value) { + return new DoubleCondition<>(this, DoubleCondition.Operation.GREATER, value); + } + + /** Creates a "less than ('<')" condition for this property. */ + public PropertyQueryCondition less(double value) { + return new DoubleCondition<>(this, DoubleCondition.Operation.LESS, value); + } + + /** Creates an "BETWEEN ... AND ..." condition for this property. */ + public PropertyQueryCondition between(double lowerBoundary, double upperBoundary) { + return new DoubleDoubleCondition<>(this, DoubleDoubleCondition.Operation.BETWEEN, + lowerBoundary, upperBoundary); + } + + /** Creates an "equal ('=')" condition for this property. */ + public PropertyQueryCondition equal(Date value) { + return new LongCondition<>(this, LongCondition.Operation.EQUAL, value); + } + + /** Creates a "not equal ('<>')" condition for this property. */ + public PropertyQueryCondition notEqual(Date value) { + return new LongCondition<>(this, LongCondition.Operation.NOT_EQUAL, value); + } + + /** Creates a "greater than ('>')" condition for this property. */ + public PropertyQueryCondition greater(Date value) { + return new LongCondition<>(this, LongCondition.Operation.GREATER, value); + } + + /** Creates a "less than ('<')" condition for this property. */ + public PropertyQueryCondition less(Date value) { + return new LongCondition<>(this, LongCondition.Operation.LESS, value); + } + + /** Creates an "BETWEEN ... AND ..." condition for this property. */ + public PropertyQueryCondition between(Date lowerBoundary, Date upperBoundary) { + return new LongLongCondition<>(this, LongLongCondition.Operation.BETWEEN, lowerBoundary, upperBoundary); + } + + /** Creates an "equal ('=')" condition for this property. */ + public PropertyQueryCondition equal(String value) { + return new StringCondition<>(this, StringCondition.Operation.EQUAL, value); + } + + /** Creates an "equal ('=')" condition for this property. */ + public PropertyQueryCondition equal(String value, StringOrder order) { + return new StringCondition<>(this, StringCondition.Operation.EQUAL, value, order); + } + + /** Creates a "not equal ('<>')" condition for this property. */ + public PropertyQueryCondition notEqual(String value) { + return new StringCondition<>(this, StringCondition.Operation.NOT_EQUAL, value); + } + + /** Creates a "not equal ('<>')" condition for this property. */ + public PropertyQueryCondition notEqual(String value, StringOrder order) { + return new StringCondition<>(this, StringCondition.Operation.NOT_EQUAL, value, order); + } + + /** Creates a "greater than ('>')" condition for this property. */ + public PropertyQueryCondition greater(String value) { + return new StringCondition<>(this, StringCondition.Operation.GREATER, value); + } + + /** Creates a "greater than ('>')" condition for this property. */ + public PropertyQueryCondition greater(String value, StringOrder order) { + return new StringCondition<>(this, StringCondition.Operation.GREATER, value, order); + } + + /** Creates a "less than ('<')" condition for this property. */ + public PropertyQueryCondition less(String value) { + return new StringCondition<>(this, StringCondition.Operation.LESS, value); + } + + /** Creates a "less than ('<')" condition for this property. */ + public PropertyQueryCondition less(String value, StringOrder order) { + return new StringCondition<>(this, StringCondition.Operation.LESS, value, order); + } + + public PropertyQueryCondition contains(String value) { + return new StringCondition<>(this, StringCondition.Operation.CONTAINS, value); + } + + public PropertyQueryCondition contains(String value, StringOrder order) { + return new StringCondition<>(this, StringCondition.Operation.CONTAINS, value, order); + } + + public PropertyQueryCondition startsWith(String value) { + return new StringCondition<>(this, Operation.STARTS_WITH, value); + } + + public PropertyQueryCondition startsWith(String value, StringOrder order) { + return new StringCondition<>(this, Operation.STARTS_WITH, value, order); + } + + public PropertyQueryCondition endsWith(String value) { + return new StringCondition<>(this, Operation.ENDS_WITH, value); + } + + public PropertyQueryCondition endsWith(String value, StringOrder order) { + return new StringCondition<>(this, Operation.ENDS_WITH, value, order); + } + + /** Creates an "IN (..., ..., ...)" condition for this property. */ + public PropertyQueryCondition oneOf(String[] values) { + return new StringArrayCondition<>(this, StringArrayCondition.Operation.IN, values); + } + + /** Creates an "IN (..., ..., ...)" condition for this property. */ + public PropertyQueryCondition oneOf(String[] values, StringOrder order) { + return new StringArrayCondition<>(this, StringArrayCondition.Operation.IN, values, order); + } + + /** Creates an "equal ('=')" condition for this property. */ + public PropertyQueryCondition equal(byte[] value) { + return new ByteArrayCondition<>(this, ByteArrayCondition.Operation.EQUAL, value); + } + + /** Creates a "greater than ('>')" condition for this property. */ + public PropertyQueryCondition greater(byte[] value) { + return new ByteArrayCondition<>(this, ByteArrayCondition.Operation.GREATER, value); + } + + /** Creates a "less than ('<')" condition for this property. */ + public PropertyQueryCondition less(byte[] value) { + return new ByteArrayCondition<>(this, ByteArrayCondition.Operation.LESS, value); } @Internal diff --git a/objectbox-java/src/main/java/io/objectbox/query/LogicQueryCondition.java b/objectbox-java/src/main/java/io/objectbox/query/LogicQueryCondition.java new file mode 100644 index 00000000..c3aa8363 --- /dev/null +++ b/objectbox-java/src/main/java/io/objectbox/query/LogicQueryCondition.java @@ -0,0 +1,58 @@ +package io.objectbox.query; + +/** + * Logic based query conditions, currently {@link AndCondition} and {@link OrCondition}. + */ +abstract class LogicQueryCondition extends QueryConditionImpl { + + private final QueryConditionImpl leftCondition; + private final QueryConditionImpl rightCondition; + + LogicQueryCondition(QueryConditionImpl leftCondition, QueryConditionImpl rightCondition) { + this.leftCondition = leftCondition; + this.rightCondition = rightCondition; + } + + @Override + void apply(QueryBuilder builder) { + leftCondition.apply(builder); + long leftConditionPointer = builder.internalGetLastCondition(); + + rightCondition.apply(builder); + long rightConditionPointer = builder.internalGetLastCondition(); + + applyOperator(builder, leftConditionPointer, rightConditionPointer); + } + + abstract void applyOperator(QueryBuilder builder, long leftCondition, long rightCondition); + + /** + * Combines the left condition using AND with the right condition. + */ + static class AndCondition extends LogicQueryCondition { + + AndCondition(QueryConditionImpl leftCondition, QueryConditionImpl rightCondition) { + super(leftCondition, rightCondition); + } + + @Override + void applyOperator(QueryBuilder builder, long leftCondition, long rightCondition) { + builder.internalAnd(leftCondition, rightCondition); + } + } + + /** + * Combines the left condition using OR with the right condition. + */ + static class OrCondition extends LogicQueryCondition { + + OrCondition(QueryConditionImpl leftCondition, QueryConditionImpl rightCondition) { + super(leftCondition, rightCondition); + } + + @Override + void applyOperator(QueryBuilder builder, long leftCondition, long rightCondition) { + builder.internalOr(leftCondition, rightCondition); + } + } +} diff --git a/objectbox-java/src/main/java/io/objectbox/query/PropertyQueryCondition.java b/objectbox-java/src/main/java/io/objectbox/query/PropertyQueryCondition.java new file mode 100644 index 00000000..a8d55387 --- /dev/null +++ b/objectbox-java/src/main/java/io/objectbox/query/PropertyQueryCondition.java @@ -0,0 +1,15 @@ +package io.objectbox.query; + +import io.objectbox.Property; + +/** + * A condition on a {@link Property}, which can have an alias to allow referring to it later. + */ +public interface PropertyQueryCondition extends QueryCondition { + + /** + * Assigns an alias to this condition that can later be used with the {@link Query} setParameter methods. + */ + QueryCondition alias(String name); + +} diff --git a/objectbox-java/src/main/java/io/objectbox/query/PropertyQueryConditionImpl.java b/objectbox-java/src/main/java/io/objectbox/query/PropertyQueryConditionImpl.java new file mode 100644 index 00000000..a98d416c --- /dev/null +++ b/objectbox-java/src/main/java/io/objectbox/query/PropertyQueryConditionImpl.java @@ -0,0 +1,379 @@ +package io.objectbox.query; + +import java.util.Date; + +import io.objectbox.Property; +import io.objectbox.query.QueryBuilder.StringOrder; + +/** + * {@link Property} based query conditions with implementations split by number and type of values, + * such as {@link LongCondition LongCondition}, {@link LongLongCondition LongLongCondition}, + * {@link LongArrayCondition LongArrayCondition} and the general {@link NullCondition NullCondition}. + *

    + * Each condition implementation has a set of operation enums, e.g. EQUAL/NOT_EQUAL/..., which represent the actual + * query condition passed to the native query builder. + */ +public abstract class PropertyQueryConditionImpl extends QueryConditionImpl implements PropertyQueryCondition { + protected final Property property; + private String alias; + + PropertyQueryConditionImpl(Property property) { + this.property = property; + } + + @Override + public QueryCondition alias(String name) { + this.alias = name; + return this; + } + + @Override + void apply(QueryBuilder builder) { + applyCondition(builder); + if (alias != null && alias.length() != 0) { + builder.parameterAlias(alias); + } + } + + abstract void applyCondition(QueryBuilder builder); + + public static class NullCondition extends PropertyQueryConditionImpl { + private final Operation op; + + public enum Operation { + IS_NULL, + NOT_NULL + } + + public NullCondition(Property property, Operation op) { + super(property); + this.op = op; + } + + @Override + void applyCondition(QueryBuilder builder) { + switch (op) { + case IS_NULL: + builder.isNull(property); + break; + case NOT_NULL: + builder.notNull(property); + break; + default: + throw new UnsupportedOperationException(op + " is not supported"); + } + } + } + + public static class IntArrayCondition extends PropertyQueryConditionImpl { + private final Operation op; + private final int[] value; + + public enum Operation { + IN, + NOT_IN + } + + public IntArrayCondition(Property property, Operation op, int[] value) { + super(property); + this.op = op; + this.value = value; + } + + @Override + void applyCondition(QueryBuilder builder) { + switch (op) { + case IN: + builder.in(property, value); + break; + case NOT_IN: + builder.notIn(property, value); + break; + default: + throw new UnsupportedOperationException(op + " is not supported for int[]"); + } + } + } + + public static class LongCondition extends PropertyQueryConditionImpl { + private final Operation op; + private final long value; + + public enum Operation { + EQUAL, + NOT_EQUAL, + GREATER, + LESS + } + + public LongCondition(Property property, Operation op, long value) { + super(property); + this.op = op; + this.value = value; + } + + public LongCondition(Property property, Operation op, boolean value) { + this(property, op, value ? 1 : 0); + } + + public LongCondition(Property property, Operation op, Date value) { + this(property, op, value.getTime()); + } + + @Override + void applyCondition(QueryBuilder builder) { + switch (op) { + case EQUAL: + builder.equal(property, value); + break; + case NOT_EQUAL: + builder.notEqual(property, value); + break; + case GREATER: + builder.greater(property, value); + break; + case LESS: + builder.less(property, value); + break; + default: + throw new UnsupportedOperationException(op + " is not supported for String"); + } + } + } + + public static class LongLongCondition extends PropertyQueryConditionImpl { + private final Operation op; + private final long leftValue; + private final long rightValue; + + public enum Operation { + BETWEEN + } + + public LongLongCondition(Property property, Operation op, long leftValue, long rightValue) { + super(property); + this.op = op; + this.leftValue = leftValue; + this.rightValue = rightValue; + } + + public LongLongCondition(Property property, Operation op, Date leftValue, Date rightValue) { + this(property, op, leftValue.getTime(), rightValue.getTime()); + } + + @Override + void applyCondition(QueryBuilder builder) { + if (op == Operation.BETWEEN) { + builder.between(property, leftValue, rightValue); + } else { + throw new UnsupportedOperationException(op + " is not supported with two long values"); + } + } + } + + public static class LongArrayCondition extends PropertyQueryConditionImpl { + private final Operation op; + private final long[] value; + + public enum Operation { + IN, + NOT_IN + } + + public LongArrayCondition(Property property, Operation op, long[] value) { + super(property); + this.op = op; + this.value = value; + } + + @Override + void applyCondition(QueryBuilder builder) { + switch (op) { + case IN: + builder.in(property, value); + break; + case NOT_IN: + builder.notIn(property, value); + break; + default: + throw new UnsupportedOperationException(op + " is not supported for long[]"); + } + } + } + + public static class DoubleCondition extends PropertyQueryConditionImpl { + private final Operation op; + private final double value; + + public enum Operation { + GREATER, + LESS + } + + public DoubleCondition(Property property, Operation op, double value) { + super(property); + this.op = op; + this.value = value; + } + + @Override + void applyCondition(QueryBuilder builder) { + switch (op) { + case GREATER: + builder.greater(property, value); + break; + case LESS: + builder.less(property, value); + break; + default: + throw new UnsupportedOperationException(op + " is not supported for double"); + } + } + } + + public static class DoubleDoubleCondition extends PropertyQueryConditionImpl { + private final Operation op; + private final double leftValue; + private final double rightValue; + + public enum Operation { + BETWEEN + } + + public DoubleDoubleCondition(Property property, Operation op, double leftValue, double rightValue) { + super(property); + this.op = op; + this.leftValue = leftValue; + this.rightValue = rightValue; + } + + @Override + void applyCondition(QueryBuilder builder) { + if (op == Operation.BETWEEN) { + builder.between(property, leftValue, rightValue); + } else { + throw new UnsupportedOperationException(op + " is not supported with two double values"); + } + } + } + + public static class StringCondition extends PropertyQueryConditionImpl { + private final Operation op; + private final String value; + private final StringOrder order; + + public enum Operation { + EQUAL, + NOT_EQUAL, + GREATER, + LESS, + CONTAINS, + STARTS_WITH, + ENDS_WITH + } + + public StringCondition(Property property, Operation op, String value, StringOrder order) { + super(property); + this.op = op; + this.value = value; + this.order = order; + } + + public StringCondition(Property property, Operation op, String value) { + this(property, op, value, StringOrder.CASE_INSENSITIVE); + } + + @Override + void applyCondition(QueryBuilder builder) { + switch (op) { + case EQUAL: + builder.equal(property, value, order); + break; + case NOT_EQUAL: + builder.notEqual(property, value, order); + break; + case GREATER: + builder.greater(property, value, order); + break; + case LESS: + builder.less(property, value, order); + break; + case CONTAINS: + builder.contains(property, value, order); + break; + case STARTS_WITH: + builder.startsWith(property, value, order); + break; + case ENDS_WITH: + builder.endsWith(property, value, order); + break; + default: + throw new UnsupportedOperationException(op + " is not supported for String"); + } + } + } + + public static class StringArrayCondition extends PropertyQueryConditionImpl { + private final Operation op; + private final String[] value; + private final StringOrder order; + + public enum Operation { + IN + } + + public StringArrayCondition(Property property, Operation op, String[] value, StringOrder order) { + super(property); + this.op = op; + this.value = value; + this.order = order; + } + + public StringArrayCondition(Property property, Operation op, String[] value) { + this(property, op, value, StringOrder.CASE_INSENSITIVE); + } + + @Override + void applyCondition(QueryBuilder builder) { + if (op == Operation.IN) { + builder.in(property, value, order); + } else { + throw new UnsupportedOperationException(op + " is not supported for String[]"); + } + } + } + + public static class ByteArrayCondition extends PropertyQueryConditionImpl { + private final Operation op; + private final byte[] value; + + public enum Operation { + EQUAL, + GREATER, + LESS + } + + public ByteArrayCondition(Property property, Operation op, byte[] value) { + super(property); + this.op = op; + this.value = value; + } + + @Override + void applyCondition(QueryBuilder builder) { + switch (op) { + case EQUAL: + builder.equal(property, value); + break; + case GREATER: + builder.greater(property, value); + break; + case LESS: + builder.less(property, value); + break; + default: + throw new UnsupportedOperationException(op + " is not supported for byte[]"); + } + } + } +} diff --git a/objectbox-java/src/main/java/io/objectbox/query/QueryBuilder.java b/objectbox-java/src/main/java/io/objectbox/query/QueryBuilder.java index 85ab52a0..da642292 100644 --- a/objectbox-java/src/main/java/io/objectbox/query/QueryBuilder.java +++ b/objectbox-java/src/main/java/io/objectbox/query/QueryBuilder.java @@ -242,6 +242,29 @@ private void verifyHandle() { } } + /** + * Applies the given query conditions and returns the builder for further customization, such as result order. + * Build the condition using the properties from your entity underscore classes. + *

    + * An example with a nested OR condition: + *

    +     * # Java
    +     * builder.apply(User_.name.equal("Jane")
    +     *         .and(User_.age.less(12)
    +     *                 .or(User_.status.equal("child"))));
    +     *
    +     * # Kotlin
    +     * builder.apply(User_.name.equal("Jane")
    +     *         and (User_.age.less(12)
    +     *         or User_.status.equal("child")))
    +     * 
    + * Use {@link Box#query(QueryCondition)} as a shortcut for this method. + */ + public QueryBuilder apply(QueryCondition queryCondition) { + ((QueryConditionImpl) queryCondition).apply(this); + return this; + } + /** * Specifies given property to be used for sorting. * Shorthand for {@link #order(Property, int)} with flags equal to 0. @@ -480,6 +503,21 @@ private void checkCombineCondition(long currentCondition) { } } + @Internal + long internalGetLastCondition() { + return lastCondition; + } + + @Internal + void internalAnd(long leftCondition, long rightCondition) { + lastCondition = nativeCombine(handle, leftCondition, rightCondition, false); + } + + @Internal + void internalOr(long leftCondition, long rightCondition) { + lastCondition = nativeCombine(handle, leftCondition, rightCondition, true); + } + public QueryBuilder isNull(Property property) { verifyHandle(); checkCombineCondition(nativeNull(handle, property.getId())); diff --git a/objectbox-java/src/main/java/io/objectbox/query/QueryCondition.java b/objectbox-java/src/main/java/io/objectbox/query/QueryCondition.java index d721820c..6553c6a7 100644 --- a/objectbox-java/src/main/java/io/objectbox/query/QueryCondition.java +++ b/objectbox-java/src/main/java/io/objectbox/query/QueryCondition.java @@ -1,270 +1,25 @@ -/* - * Copyright 2017 ObjectBox Ltd. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package io.objectbox.query; -import java.util.Date; - -import javax.annotation.Nullable; - import io.objectbox.Property; -import io.objectbox.annotation.apihint.Experimental; -import io.objectbox.annotation.apihint.Internal; -import io.objectbox.exception.DbException; -import io.objectbox.query.QueryBuilder.StringOrder; /** - * Internal interface to model WHERE conditions used in queries. Use the {@link Property} objects in the DAO classes to - * create new conditions. + * Allows building queries with a fluent interface. Use through {@link io.objectbox.Box#query(QueryCondition)} + * and build a condition with {@link Property} methods. */ -@Experimental -@Internal -public interface QueryCondition { - - void applyTo(QueryBuilder queryBuilder, StringOrder stringOrder); - - void setParameterFor(Query query, Object parameter); - - void setParameterFor(Query query, Object parameter1, Object parameter2); - - abstract class AbstractCondition implements QueryCondition { - - public final Object value; - protected final Object[] values; - - AbstractCondition(Object value) { - this.value = value; - this.values = null; - } - - AbstractCondition(@Nullable Object[] values) { - this.value = null; - this.values = values; - } - - } - - class PropertyCondition extends AbstractCondition { - - public enum Operation { - EQUALS, - NOT_EQUALS, - BETWEEN, - IN, - GREATER_THAN, - LESS_THAN, - IS_NULL, - IS_NOT_NULL, - CONTAINS, - STARTS_WITH, - ENDS_WITH - } - - public final Property property; - private final Operation operation; - - public PropertyCondition(Property property, Operation operation, @Nullable Object value) { - super(checkValueForType(property, value)); - this.property = property; - this.operation = operation; - } - - public PropertyCondition(Property property, Operation operation, @Nullable Object[] values) { - super(checkValuesForType(property, operation, values)); - this.property = property; - this.operation = operation; - } - - public void applyTo(QueryBuilder queryBuilder, StringOrder stringOrder) { - if (operation == Operation.EQUALS) { - if (value instanceof Long) { - queryBuilder.equal(property, (Long) value); - } else if (value instanceof Integer) { - queryBuilder.equal(property, (Integer) value); - } else if (value instanceof String) { - queryBuilder.equal(property, (String) value, stringOrder); - } - } else if (operation == Operation.NOT_EQUALS) { - if (value instanceof Long) { - queryBuilder.notEqual(property, (Long) value); - } else if (value instanceof Integer) { - queryBuilder.notEqual(property, (Integer) value); - } else if (value instanceof String) { - queryBuilder.notEqual(property, (String) value, stringOrder); - } - } else if (operation == Operation.BETWEEN) { - if (values[0] instanceof Long && values[1] instanceof Long) { - queryBuilder.between(property, (Long) values[0], (Long) values[1]); - } else if (values[0] instanceof Integer && values[1] instanceof Integer) { - queryBuilder.between(property, (Integer) values[0], (Integer) values[1]); - } else if (values[0] instanceof Double && values[1] instanceof Double) { - queryBuilder.between(property, (Double) values[0], (Double) values[1]); - } else if (values[0] instanceof Float && values[1] instanceof Float) { - queryBuilder.between(property, (Float) values[0], (Float) values[1]); - } - } else if (operation == Operation.IN) { - // just check the first value and assume all others are of the same type - // maybe this is too naive and we should properly check values earlier - if (values[0] instanceof Long) { - long[] inValues = new long[values.length]; - for (int i = 0; i < values.length; i++) { - inValues[i] = (long) values[i]; - } - queryBuilder.in(property, inValues); - } else if (values[0] instanceof Integer) { - int[] inValues = new int[values.length]; - for (int i = 0; i < values.length; i++) { - inValues[i] = (int) values[i]; - } - queryBuilder.in(property, inValues); - } - } else if (operation == Operation.GREATER_THAN) { - if (value instanceof Long) { - queryBuilder.greater(property, (Long) value); - } else if (value instanceof Integer) { - queryBuilder.greater(property, (Integer) value); - } else if (value instanceof Double) { - queryBuilder.greater(property, (Double) value); - } else if (value instanceof Float) { - queryBuilder.greater(property, (Float) value); - } - } else if (operation == Operation.LESS_THAN) { - if (value instanceof Long) { - queryBuilder.less(property, (Long) value); - } else if (value instanceof Integer) { - queryBuilder.less(property, (Integer) value); - } else if (value instanceof Double) { - queryBuilder.less(property, (Double) value); - } else if (value instanceof Float) { - queryBuilder.less(property, (Float) value); - } - } else if (operation == Operation.IS_NULL) { - queryBuilder.isNull(property); - } else if (operation == Operation.IS_NOT_NULL) { - queryBuilder.notNull(property); - } else if (operation == Operation.CONTAINS) { - // no need for greenDAO compat, so only String was allowed - queryBuilder.contains(property, (String) value, stringOrder); - } else if (operation == Operation.STARTS_WITH) { - // no need for greenDAO compat, so only String was allowed - queryBuilder.startsWith(property, (String) value, stringOrder); - } else if (operation == Operation.ENDS_WITH) { - // no need for greenDAO compat, so only String was allowed - queryBuilder.endsWith(property, (String) value, stringOrder); - } else { - throw new UnsupportedOperationException("This operation is not known."); - } - } - - private static Object checkValueForType(Property property, @Nullable Object value) { - if (value != null && value.getClass().isArray()) { - throw new DbException("Illegal value: found array, but simple object required"); - } - Class type = property.type; - if (type == Date.class) { - if (value instanceof Date) { - return ((Date) value).getTime(); - } else if (value instanceof Long) { - return value; - } else { - throw new DbException("Illegal date value: expected java.util.Date or Long for value " + value); - } - } else if (property.type == boolean.class || property.type == Boolean.class) { - if (value instanceof Boolean) { - return ((Boolean) value) ? 1 : 0; - } else if (value instanceof Number) { - int intValue = ((Number) value).intValue(); - if (intValue != 0 && intValue != 1) { - throw new DbException("Illegal boolean value: numbers must be 0 or 1, but was " + value); - } - } else if (value instanceof String) { - String stringValue = ((String) value); - if ("TRUE".equalsIgnoreCase(stringValue)) { - return 1; - } else if ("FALSE".equalsIgnoreCase(stringValue)) { - return 0; - } else { - throw new DbException( - "Illegal boolean value: Strings must be \"TRUE\" or \"FALSE\" (case insensitive), but was " - + value); - } - } - } - return value; - } - - private static Object[] checkValuesForType(Property property, Operation operation, @Nullable Object[] values) { - if (values == null) { - if (operation == Operation.IS_NULL || operation == Operation.IS_NOT_NULL) { - return null; - } else { - throw new IllegalArgumentException("This operation requires non-null values."); - } - } - for (int i = 0; i < values.length; i++) { - values[i] = checkValueForType(property, values[i]); - } - return values; - } - - @Override - public void setParameterFor(Query query, Object parameter) { - if (parameter == null) { - throw new IllegalArgumentException("The new parameter can not be null."); - } - if (operation == Operation.BETWEEN) { - throw new UnsupportedOperationException("The BETWEEN condition requires two parameters."); - } - if (operation == Operation.IN) { - throw new UnsupportedOperationException("The IN condition does not support changing parameters."); - } - if (parameter instanceof Long) { - query.setParameter(property, (Long) parameter); - } else if (parameter instanceof Integer) { - query.setParameter(property, (Integer) parameter); - } else if (parameter instanceof String) { - query.setParameter(property, (String) parameter); - } else if (parameter instanceof Double) { - query.setParameter(property, (Double) parameter); - } else if (parameter instanceof Float) { - query.setParameter(property, (Float) parameter); - } else { - throw new IllegalArgumentException("Only LONG, INTEGER, DOUBLE, FLOAT or STRING parameters are supported."); - } - } - - @Override - public void setParameterFor(Query query, Object parameter1, Object parameter2) { - if (parameter1 == null || parameter2 == null) { - throw new IllegalArgumentException("The new parameters can not be null."); - } - if (operation != Operation.BETWEEN) { - throw new UnsupportedOperationException("Only the BETWEEN condition supports two parameters."); - } - if (parameter1 instanceof Long && parameter2 instanceof Long) { - query.setParameters(property, (Long) parameter1, (Long) parameter2); - } else if (parameter1 instanceof Integer && parameter2 instanceof Integer) { - query.setParameters(property, (Integer) parameter1, (Integer) parameter2); - } else if (parameter1 instanceof Double && parameter2 instanceof Double) { - query.setParameters(property, (Double) parameter1, (Double) parameter2); - } else if (parameter1 instanceof Float && parameter2 instanceof Float) { - query.setParameters(property, (Float) parameter1, (Float) parameter2); - } else { - throw new IllegalArgumentException("The BETWEEN condition only supports LONG, INTEGER, DOUBLE or FLOAT parameters."); - } - } - } +public interface QueryCondition { + + /** + * Combines this condition using AND with the given condition. + * + * @see #or(QueryCondition) + */ + QueryCondition and(QueryCondition queryCondition); + + /** + * Combines this condition using OR with the given condition. + * + * @see #and(QueryCondition) + */ + QueryCondition or(QueryCondition queryCondition); } diff --git a/objectbox-java/src/main/java/io/objectbox/query/QueryConditionImpl.java b/objectbox-java/src/main/java/io/objectbox/query/QueryConditionImpl.java new file mode 100644 index 00000000..5fe8bc61 --- /dev/null +++ b/objectbox-java/src/main/java/io/objectbox/query/QueryConditionImpl.java @@ -0,0 +1,22 @@ +package io.objectbox.query; + +import io.objectbox.query.LogicQueryCondition.AndCondition; +import io.objectbox.query.LogicQueryCondition.OrCondition; + +/** + * Hides the {@link #apply(QueryBuilder)} method from the public API ({@link QueryCondition}). + */ +abstract class QueryConditionImpl implements QueryCondition { + + @Override + public QueryCondition and(QueryCondition queryCondition) { + return new AndCondition<>(this, (QueryConditionImpl) queryCondition); + } + + @Override + public QueryCondition or(QueryCondition queryCondition) { + return new OrCondition<>(this, (QueryConditionImpl) queryCondition); + } + + abstract void apply(QueryBuilder builder); +} diff --git a/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Extensions.kt b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Extensions.kt index 8b01fb31..e6880afc 100644 --- a/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Extensions.kt +++ b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Extensions.kt @@ -23,6 +23,7 @@ import io.objectbox.BoxStore import io.objectbox.Property import io.objectbox.query.Query import io.objectbox.query.QueryBuilder +import io.objectbox.query.QueryCondition import io.objectbox.relation.ToMany import kotlin.reflect.KClass @@ -134,7 +135,7 @@ inline fun QueryBuilder.between(property: Property, value1: Fl * } * ``` */ -inline fun Box.query(block: QueryBuilder.() -> Unit) : Query { +inline fun Box.query(block: QueryBuilder.() -> Unit): Query { val builder = query() block(builder) return builder.build() @@ -155,3 +156,21 @@ inline fun ToMany.applyChangesToDb(resetFirst: Boolean = false, body: ToM body() applyChangesToDb() } + +/** + * Combines the left hand side condition using AND with the right hand side condition. + * + * @see or + */ +infix fun QueryCondition.and(queryCondition: QueryCondition): QueryCondition { + return and(queryCondition) +} + +/** + * Combines the left hand side condition using OR with the right hand side condition. + * + * @see and + */ +infix fun QueryCondition.or(queryCondition: QueryCondition): QueryCondition { + return or(queryCondition) +} From 9a806b84c327d9acd21a25159f80eb563314aa1a Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 3 Mar 2020 08:23:06 +0100 Subject: [PATCH 09/34] Apply Kotlin plugin to test project, store versions in root build.gradle. --- build.gradle | 10 ++++++++++ objectbox-kotlin/build.gradle | 12 ------------ tests/objectbox-java-test/build.gradle | 2 ++ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index 46e6ad66..0732cc2a 100644 --- a/build.gradle +++ b/build.gradle @@ -28,12 +28,22 @@ buildscript { objectboxNativeDependency = "io.objectbox:objectbox-$objectboxPlatform:$ob_native_version" println "ObjectBox native dependency: $objectboxNativeDependency" } + + ext.kotlin_version = '1.3.61' + ext.dokka_version = '0.10.1' ext.junit_version = '4.13' repositories { mavenCentral() jcenter() } + + dependencies { + // Add Kotlin plugins once. Apply in sub-projects as required. + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" + } + } allprojects { diff --git a/objectbox-kotlin/build.gradle b/objectbox-kotlin/build.gradle index e29c8ffc..f4221f1f 100644 --- a/objectbox-kotlin/build.gradle +++ b/objectbox-kotlin/build.gradle @@ -3,18 +3,6 @@ version= rootProject.version buildscript { ext.javadocDir = "$buildDir/docs/javadoc" - ext.kotlin_version = '1.3.61' - ext.dokka_version = '0.10.1' - - repositories { - mavenCentral() - jcenter() - } - - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" - } } apply plugin: 'kotlin' diff --git a/tests/objectbox-java-test/build.gradle b/tests/objectbox-java-test/build.gradle index a0d4234a..8aece49f 100644 --- a/tests/objectbox-java-test/build.gradle +++ b/tests/objectbox-java-test/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'java' +apply plugin: 'kotlin' uploadArchives.enabled = false @@ -23,6 +24,7 @@ repositories { dependencies { compile project(':objectbox-java') + compile project(':objectbox-kotlin') compile 'org.greenrobot:essentials:3.0.0-RC1' // Check flag to use locally compiled version to avoid dependency cycles From e62e987c41a377a8c444805a7f9feb3cd90a5f43 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 3 Mar 2020 08:49:03 +0100 Subject: [PATCH 10/34] Add some tests for the new query API. --- .../io/objectbox/FunctionalTestSuite.java | 4 + .../java/io/objectbox/query/QueryTest.java | 2 +- .../java/io/objectbox/query/QueryTest2.java | 121 +++++++++++++++++ .../java/io/objectbox/query/QueryTestK.kt | 125 ++++++++++++++++++ 4 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest2.java create mode 100644 tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/FunctionalTestSuite.java b/tests/objectbox-java-test/src/test/java/io/objectbox/FunctionalTestSuite.java index b7f3c8b5..3a7d191b 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/FunctionalTestSuite.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/FunctionalTestSuite.java @@ -22,6 +22,8 @@ import io.objectbox.query.QueryFilterComparatorTest; import io.objectbox.query.QueryObserverTest; import io.objectbox.query.QueryTest; +import io.objectbox.query.QueryTest2; +import io.objectbox.query.QueryTestK; import io.objectbox.relation.RelationEagerTest; import io.objectbox.relation.RelationTest; import io.objectbox.relation.ToManyStandaloneTest; @@ -48,6 +50,8 @@ QueryFilterComparatorTest.class, QueryObserverTest.class, QueryTest.class, + QueryTest2.class, + QueryTestK.class, RelationTest.class, RelationEagerTest.class, ToManyStandaloneTest.class, diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest.java b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest.java index 68d43a84..54d4090f 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest.java @@ -507,7 +507,7 @@ public void testOr_bad2() { @Test public void testAnd() { putTestEntitiesScalars(); - // OR precedence (wrong): {}, AND precedence (expected): 2008 + // Result if OR precedence (wrong): {}, AND precedence (expected): {2008} Query query = box.query().equal(simpleInt, 2006).and().equal(simpleInt, 2007).or().equal(simpleInt, 2008).build(); List entities = query.find(); assertEquals(1, entities.size()); diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest2.java b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest2.java new file mode 100644 index 00000000..35daaf7a --- /dev/null +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest2.java @@ -0,0 +1,121 @@ +/* + * Copyright 2017 ObjectBox Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.objectbox.query; + +import org.junit.Test; + +import java.util.List; + +import io.objectbox.TestEntity; +import io.objectbox.TestEntity_; + + +import static io.objectbox.TestEntity_.simpleInt; +import static io.objectbox.TestEntity_.simpleLong; +import static org.junit.Assert.assertEquals; + +/** + * Tests {@link Query} using the new query builder API with nesting support. + */ +public class QueryTest2 extends AbstractQueryTest { + + @Test + public void newQueryApi() { + putTestEntity("Fry", 14); + putTestEntity("Fry", 12); + putTestEntity("Fry", 10); + + // current query API + Query query = box.query() + .equal(TestEntity_.simpleString, "Fry") + .less(TestEntity_.simpleInt, 12) + .or() + .in(TestEntity_.simpleLong, new long[]{1012}) + .order(TestEntity_.simpleInt) + .build(); + + List results = query.find(); + assertEquals(2, results.size()); + assertEquals(10, results.get(0).getSimpleInt()); + assertEquals(12, results.get(1).getSimpleInt()); + + // suggested query API + Query newQuery = box.query( + TestEntity_.simpleString.equal("Fry") + .and(TestEntity_.simpleInt.less(12) + .or(TestEntity_.simpleLong.oneOf(new long[]{1012})))) + .order(TestEntity_.simpleInt) + .build(); + + List newResults = newQuery.find(); + assertEquals(2, newResults.size()); + assertEquals(10, newResults.get(0).getSimpleInt()); + assertEquals(12, newResults.get(1).getSimpleInt()); + } + + @Test + public void parenthesesMatter() { + putTestEntity("Fry", 14); + putTestEntity("Fry", 12); + putTestEntity("Fry", 10); + + // Nested OR + // (EQ OR EQ) AND LESS + List resultsNestedOr = box.query( + TestEntity_.simpleString.equal("Fry") + .or(TestEntity_.simpleString.equal("Sarah")) + .and(TestEntity_.simpleInt.less(12)) + ).build().find(); + // Only the Fry age 10. + assertEquals(1, resultsNestedOr.size()); + assertEquals(10, resultsNestedOr.get(0).getSimpleInt()); + + // Nested AND + // EQ OR (EQ AND LESS) + List resultsNestedAnd = box.query( + TestEntity_.simpleString.equal("Fry") + .or(TestEntity_.simpleString.equal("Sarah") + .and(TestEntity_.simpleInt.less(12))) + ).build().find(); + // All three Fry's. + assertEquals(3, resultsNestedAnd.size()); + } + + @Test + public void or() { + putTestEntitiesScalars(); + Query query = box.query(simpleInt.equal(2007).or(simpleLong.equal(3002))).build(); + List entities = query.find(); + assertEquals(2, entities.size()); + assertEquals(3002, entities.get(0).getSimpleLong()); + assertEquals(2007, entities.get(1).getSimpleInt()); + } + + @Test + public void and() { + putTestEntitiesScalars(); + // Result if OR precedence (wrong): {}, AND precedence (expected): {2008} + Query query = box.query(TestEntity_.simpleInt.equal(2006) + .and(TestEntity_.simpleInt.equal(2007)) + .or(TestEntity_.simpleInt.equal(2008))) + .build(); + List entities = query.find(); + assertEquals(1, entities.size()); + assertEquals(2008, entities.get(0).getSimpleInt()); + } + +} diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt new file mode 100644 index 00000000..aad2555b --- /dev/null +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt @@ -0,0 +1,125 @@ +package io.objectbox.query + +import io.objectbox.TestEntity_ +import io.objectbox.kotlin.and +import io.objectbox.kotlin.inValues +import io.objectbox.kotlin.or +import io.objectbox.kotlin.query +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Preliminary tests and playground for new query API. At last should duplicate all query tests with new API. + * + * Note: expanding Java tests for now as we still have major Java users, see [QueryTest2]. + */ +class QueryTestK : AbstractQueryTest() { + + @Test + fun newQueryApi() { + putTestEntity("Fry", 14) + putTestEntity("Fry", 12) + putTestEntity("Fry", 10) + + // current query API + val query = box.query { + less(TestEntity_.simpleInt, 12) + or() + inValues(TestEntity_.simpleLong, longArrayOf(1012)) + equal(TestEntity_.simpleString, "Fry") + order(TestEntity_.simpleInt) + } + val results = query.find() + assertEquals(2, results.size) + assertEquals(10, results[0].simpleInt) + assertEquals(12, results[1].simpleInt) + + // suggested query API + val newQuery = box.query( + (TestEntity_.simpleInt.less(12) or TestEntity_.simpleLong.oneOf(longArrayOf(1012))) + and TestEntity_.simpleString.equal("Fry") + ).order(TestEntity_.simpleInt).build() + val resultsNew = newQuery.find() + assertEquals(2, resultsNew.size) + assertEquals(10, resultsNew[0].simpleInt) + assertEquals(12, resultsNew[1].simpleInt) + + val newQueryOr = box.query( + // (EQ OR EQ) AND LESS + (TestEntity_.simpleString.equal("Fry") or TestEntity_.simpleString.equal("Sarah")) + and TestEntity_.simpleInt.less(12) + ).build().find() + assertEquals(1, newQueryOr.size) // only the Fry age 10 + + val newQueryAnd = box.query( + // EQ OR (EQ AND LESS) + TestEntity_.simpleString.equal("Fry") or + (TestEntity_.simpleString.equal("Sarah") and TestEntity_.simpleInt.less(12)) + ).build().find() + assertEquals(3, newQueryAnd.size) // all Fry's + } + + @Test + fun intLessAndGreater() { + putTestEntitiesScalars() + val query = box.query( + TestEntity_.simpleInt.greater(2003) + and TestEntity_.simpleShort.less(2107) + ).build() + assertEquals(3, query.count()) + } + + @Test + fun intBetween() { + putTestEntitiesScalars() + val query = box.query( + TestEntity_.simpleInt.between(2003, 2006) + ).build() + assertEquals(4, query.count()) + } + + @Test + fun intOneOf() { + putTestEntitiesScalars() + + val valuesInt = intArrayOf(1, 1, 2, 3, 2003, 2007, 2002, -1) + val query = box.query( + TestEntity_.simpleInt.oneOf(valuesInt).alias("int") + ).build() + assertEquals(3, query.count()) + + val valuesInt2 = intArrayOf(2003) + query.setParameters(TestEntity_.simpleInt, valuesInt2) + assertEquals(1, query.count()) + + val valuesInt3 = intArrayOf(2003, 2007) + query.setParameters("int", valuesInt3) + assertEquals(2, query.count()) + } + + + @Test + fun or() { + putTestEntitiesScalars() + val query = box.query( + TestEntity_.simpleInt.equal(2007) or TestEntity_.simpleLong.equal(3002) + ).build() + val entities = query.find() + assertEquals(2, entities.size.toLong()) + assertEquals(3002, entities[0].simpleLong) + assertEquals(2007, entities[1].simpleInt.toLong()) + } + + @Test + fun and() { + putTestEntitiesScalars() + // Result if OR precedence (wrong): {}, AND precedence (expected): {2008} + val query = box.query( + TestEntity_.simpleInt.equal(2006) and TestEntity_.simpleInt.equal(2007) or TestEntity_.simpleInt.equal(2008) + ).build() + val entities = query.find() + assertEquals(1, entities.size.toLong()) + assertEquals(2008, entities[0].simpleInt.toLong()) + } + +} \ No newline at end of file From 17a825a45b3b51e8e38375e668f50b3e1e86fe61 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 21 Jan 2019 15:15:40 +0100 Subject: [PATCH 11/34] Cursor: add collectStringArray to support PropertyType.StringVector --- objectbox-java/src/main/java/io/objectbox/Cursor.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/objectbox-java/src/main/java/io/objectbox/Cursor.java b/objectbox-java/src/main/java/io/objectbox/Cursor.java index f08bb1d5..177c82c0 100644 --- a/objectbox-java/src/main/java/io/objectbox/Cursor.java +++ b/objectbox-java/src/main/java/io/objectbox/Cursor.java @@ -106,6 +106,10 @@ protected static native long collect004000(long cursor, long keyIfComplete, int int idLong3, long valueLong3, int idLong4, long valueLong4 ); + protected static native long collectStringArray(long cursor, long keyIfComplete, int flags, + int idStringArray, String[] stringArray + ); + native int nativePropertyId(long cursor, String propertyValue); native List nativeGetBacklinkEntities(long cursor, int entityId, int propertyId, long key); From ac5ab7b61aecb5d513995206139f63791ba7abc2 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 22 Jan 2019 11:27:29 +0100 Subject: [PATCH 12/34] TestEntity: add String array property and test. - Expand BoxTest.testPutAndGet() to put and assert all properties. - Add to NonArgConstructorTest. --- .../main/java/io/objectbox/TestEntity.java | 14 ++++++++++- .../java/io/objectbox/TestEntityCursor.java | 9 +++++++- .../main/java/io/objectbox/TestEntity_.java | 10 +++++--- .../io/objectbox/AbstractObjectBoxTest.java | 23 +++++++++++++++++++ .../src/test/java/io/objectbox/BoxTest.java | 9 +++++--- .../io/objectbox/NonArgConstructorTest.java | 8 ++++--- 6 files changed, 62 insertions(+), 11 deletions(-) diff --git a/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntity.java b/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntity.java index c1176677..92cc9d48 100644 --- a/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntity.java +++ b/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntity.java @@ -32,6 +32,8 @@ public class TestEntity { private String simpleString; /** Not-null value. */ private byte[] simpleByteArray; + /** Not-null value. */ + private String[] simpleStringArray; /** In "real" entity would be annotated with @Unsigned. */ private short simpleShortU; /** In "real" entity would be annotated with @Unsigned. */ @@ -49,7 +51,7 @@ public TestEntity(long id) { this.id = id; } - public TestEntity(long id, boolean simpleBoolean, byte simpleByte, short simpleShort, int simpleInt, long simpleLong, float simpleFloat, double simpleDouble, String simpleString, byte[] simpleByteArray, short simpleShortU, int simpleIntU, long simpleLongU) { + public TestEntity(long id, boolean simpleBoolean, byte simpleByte, short simpleShort, int simpleInt, long simpleLong, float simpleFloat, double simpleDouble, String simpleString, byte[] simpleByteArray, String[] simpleStringArray, short simpleShortU, int simpleIntU, long simpleLongU) { this.id = id; this.simpleBoolean = simpleBoolean; this.simpleByte = simpleByte; @@ -60,6 +62,7 @@ public TestEntity(long id, boolean simpleBoolean, byte simpleByte, short simpleS this.simpleDouble = simpleDouble; this.simpleString = simpleString; this.simpleByteArray = simpleByteArray; + this.simpleStringArray = simpleStringArray; this.simpleShortU = simpleShortU; this.simpleIntU = simpleIntU; this.simpleLongU = simpleLongU; @@ -149,6 +152,15 @@ public void setSimpleByteArray(byte[] simpleByteArray) { this.simpleByteArray = simpleByteArray; } + /** Not-null value. */ + public String[] getSimpleStringArray() { + return simpleStringArray; + } + + /** Not-null value; ensure this value is available before it is saved to the database. */ + public void setSimpleStringArray(String[] simpleStringArray) { + this.simpleStringArray = simpleStringArray; + } public short getSimpleShortU() { return simpleShortU; } diff --git a/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntityCursor.java b/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntityCursor.java index 55752103..b0a0fa3e 100644 --- a/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntityCursor.java +++ b/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntityCursor.java @@ -51,6 +51,7 @@ public Cursor createCursor(io.objectbox.Transaction tx, long cursorH private final static int __ID_simpleDouble = TestEntity_.simpleDouble.id; private final static int __ID_simpleString = TestEntity_.simpleString.id; private final static int __ID_simpleByteArray = TestEntity_.simpleByteArray.id; + private final static int __ID_simpleStringArray = TestEntity_.simpleStringArray.id; private final static int __ID_simpleShortU = TestEntity_.simpleShortU.id; private final static int __ID_simpleIntU = TestEntity_.simpleIntU.id; private final static int __ID_simpleLongU = TestEntity_.simpleLongU.id; @@ -71,12 +72,18 @@ public final long getId(TestEntity entity) { */ @Override public final long put(TestEntity entity) { + String[] simpleStringArray = entity.getSimpleStringArray(); + int __id10 = simpleStringArray != null ? __ID_simpleStringArray : 0; + + collectStringArray(cursor, 0, PUT_FLAG_FIRST, + __id10, simpleStringArray); + String simpleString = entity.getSimpleString(); int __id8 = simpleString != null ? __ID_simpleString : 0; byte[] simpleByteArray = entity.getSimpleByteArray(); int __id9 = simpleByteArray != null ? __ID_simpleByteArray : 0; - collect313311(cursor, 0, PUT_FLAG_FIRST, + collect313311(cursor, 0, 0, __id8, simpleString, 0, null, 0, null, __id9, simpleByteArray, __ID_simpleLong, entity.getSimpleLong(), __ID_simpleLongU, entity.getSimpleLongU(), diff --git a/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntity_.java b/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntity_.java index 1bc39153..817a570c 100644 --- a/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntity_.java +++ b/tests/objectbox-java-test/src/main/java/io/objectbox/TestEntity_.java @@ -77,14 +77,17 @@ public final class TestEntity_ implements EntityInfo { public final static io.objectbox.Property simpleByteArray = new io.objectbox.Property<>(__INSTANCE, 9, 10, byte[].class, "simpleByteArray"); + public final static io.objectbox.Property simpleStringArray = + new io.objectbox.Property<>(__INSTANCE, 10, 11, String[].class, "simpleStringArray", false, "simpleStringArray"); + public final static io.objectbox.Property simpleShortU = - new io.objectbox.Property<>(__INSTANCE, 10, 11, short.class, "simpleShortU"); + new io.objectbox.Property<>(__INSTANCE, 11, 12, short.class, "simpleShortU"); public final static io.objectbox.Property simpleIntU = - new io.objectbox.Property<>(__INSTANCE, 11, 12, int.class, "simpleIntU"); + new io.objectbox.Property<>(__INSTANCE, 12, 13, int.class, "simpleIntU"); public final static io.objectbox.Property simpleLongU = - new io.objectbox.Property<>(__INSTANCE, 12, 13, long.class, "simpleLongU"); + new io.objectbox.Property<>(__INSTANCE, 13, 14, long.class, "simpleLongU"); @SuppressWarnings("unchecked") public final static io.objectbox.Property[] __ALL_PROPERTIES = new io.objectbox.Property[]{ @@ -98,6 +101,7 @@ public final class TestEntity_ implements EntityInfo { simpleDouble, simpleString, simpleByteArray, + simpleStringArray, simpleShortU, simpleIntU, simpleLongU diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/AbstractObjectBoxTest.java b/tests/objectbox-java-test/src/test/java/io/objectbox/AbstractObjectBoxTest.java index 42937356..e97b9903 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/AbstractObjectBoxTest.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/AbstractObjectBoxTest.java @@ -34,6 +34,8 @@ import io.objectbox.model.PropertyFlags; import io.objectbox.model.PropertyType; + +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -197,6 +199,7 @@ private void addTestEntity(ModelBuilder modelBuilder, boolean withIndex) { pb.flags(PropertyFlags.INDEXED).indexId(++lastIndexId, lastIndexUid); } entityBuilder.property("simpleByteArray", PropertyType.ByteVector).id(TestEntity_.simpleByteArray.id, ++lastUid); + entityBuilder.property("simpleStringArray", PropertyType.StringVector).id(TestEntity_.simpleStringArray.id, ++lastUid); // Unsigned integers. entityBuilder.property("simpleShortU", PropertyType.Short).id(TestEntity_.simpleShortU.id, ++lastUid) @@ -241,12 +244,32 @@ protected TestEntity createTestEntity(@Nullable String simpleString, int nr) { entity.setSimpleFloat(200 + nr / 10f); entity.setSimpleDouble(2000 + nr / 100f); entity.setSimpleByteArray(new byte[]{1, 2, (byte) nr}); + entity.setSimpleStringArray(new String[]{simpleString}); entity.setSimpleShortU((short) (100 + nr)); entity.setSimpleIntU(nr); entity.setSimpleLongU(1000 + nr); return entity; } + /** + * Asserts all properties, excluding id. Assumes entity was created with {@link #createTestEntity(String, int)}. + */ + protected void assertTestEntity(TestEntity actual, @Nullable String simpleString, int nr) { + assertEquals(simpleString, actual.getSimpleString()); + assertEquals(nr, actual.getSimpleInt()); + assertEquals((byte) (10 + nr), actual.getSimpleByte()); + assertEquals(nr % 2 == 0, actual.getSimpleBoolean()); + assertEquals((short) (100 + nr), actual.getSimpleShort()); + assertEquals(1000 + nr, actual.getSimpleLong()); + assertEquals(200 + nr / 10f, actual.getSimpleFloat(), 0); + assertEquals(2000 + nr / 100f, actual.getSimpleDouble(), 0); + assertArrayEquals(new byte[]{1, 2, (byte) nr}, actual.getSimpleByteArray()); + assertArrayEquals(new String[]{simpleString}, actual.getSimpleStringArray()); + assertEquals((short) (100 + nr), actual.getSimpleShortU()); + assertEquals(nr, actual.getSimpleIntU()); + assertEquals(1000 + nr, actual.getSimpleLongU()); + } + protected TestEntity putTestEntity(@Nullable String simpleString, int nr) { TestEntity entity = createTestEntity(simpleString, nr); long key = getTestEntityBox().put(entity); diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/BoxTest.java b/tests/objectbox-java-test/src/test/java/io/objectbox/BoxTest.java index c792a613..39a8d851 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/BoxTest.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/BoxTest.java @@ -37,15 +37,18 @@ public void setUpBox() { @Test public void testPutAndGet() { - TestEntity entity = new TestEntity(); - entity.setSimpleInt(1977); + final String simpleString = "sunrise"; + final int simpleInt = 1977; + + TestEntity entity = createTestEntity(simpleString, simpleInt); long key = box.put(entity); assertTrue(key != 0); assertEquals(key, entity.getId()); TestEntity entityRead = box.get(key); assertNotNull(entityRead); - assertEquals(1977, entityRead.getSimpleInt()); + assertEquals(key, entityRead.getId()); + assertTestEntity(entityRead, simpleString, simpleInt); } @Test diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/NonArgConstructorTest.java b/tests/objectbox-java-test/src/test/java/io/objectbox/NonArgConstructorTest.java index e6c4e3bc..cfd36959 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/NonArgConstructorTest.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/NonArgConstructorTest.java @@ -19,12 +19,11 @@ import org.junit.Before; import org.junit.Test; -import java.util.Arrays; - import io.objectbox.ModelBuilder.EntityBuilder; import io.objectbox.model.EntityFlags; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -56,6 +55,8 @@ public void testPutAndGet() { entity.setSimpleLong(789437444354L); entity.setSimpleShort((short) 233); entity.setSimpleString("foo"); + String[] strings = {"foo", "bar"}; + entity.setSimpleStringArray(strings); long key = box.put(entity); TestEntity entityRead = box.get(key); @@ -68,8 +69,9 @@ public void testPutAndGet() { assertEquals(789437444354L, entityRead.getSimpleLong()); assertEquals(3.14f, entityRead.getSimpleFloat(), 0.000001f); assertEquals(3.141f, entityRead.getSimpleDouble(), 0.000001); - assertTrue(Arrays.equals(bytes, entityRead.getSimpleByteArray())); + assertArrayEquals(bytes, entityRead.getSimpleByteArray()); assertEquals("foo", entityRead.getSimpleString()); + assertArrayEquals(strings, entityRead.getSimpleStringArray()); } } From ec5513585202c7f20fefe7c3ddc2bd398ff28da5 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 22 Jan 2019 15:32:49 +0100 Subject: [PATCH 13/34] Verify String array with a null item or only null item works. --- .../io/objectbox/AbstractObjectBoxTest.java | 4 ++- .../src/test/java/io/objectbox/BoxTest.java | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/AbstractObjectBoxTest.java b/tests/objectbox-java-test/src/test/java/io/objectbox/AbstractObjectBoxTest.java index e97b9903..c8d167b0 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/AbstractObjectBoxTest.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/AbstractObjectBoxTest.java @@ -264,7 +264,9 @@ protected void assertTestEntity(TestEntity actual, @Nullable String simpleString assertEquals(200 + nr / 10f, actual.getSimpleFloat(), 0); assertEquals(2000 + nr / 100f, actual.getSimpleDouble(), 0); assertArrayEquals(new byte[]{1, 2, (byte) nr}, actual.getSimpleByteArray()); - assertArrayEquals(new String[]{simpleString}, actual.getSimpleStringArray()); + // null array items are ignored, so array will be empty + String[] expectedStringArray = simpleString == null ? new String[]{} : new String[]{simpleString}; + assertArrayEquals(expectedStringArray, actual.getSimpleStringArray()); assertEquals((short) (100 + nr), actual.getSimpleShortU()); assertEquals(nr, actual.getSimpleIntU()); assertEquals(1000 + nr, actual.getSimpleLongU()); diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/BoxTest.java b/tests/objectbox-java-test/src/test/java/io/objectbox/BoxTest.java index 39a8d851..a8b373bc 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/BoxTest.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/BoxTest.java @@ -51,6 +51,34 @@ public void testPutAndGet() { assertTestEntity(entityRead, simpleString, simpleInt); } + @Test + public void testPutStringArray_withNull_ignoresNull() { + final String[] stringArray = new String[]{"sunrise", null, "sunset"}; + final String[] expectedStringArray = new String[]{"sunrise", "sunset"}; + + TestEntity entity = new TestEntity(); + entity.setSimpleStringArray(stringArray); + box.put(entity); + + TestEntity entityRead = box.get(entity.getId()); + assertNotNull(entityRead); + assertArrayEquals(expectedStringArray, entityRead.getSimpleStringArray()); + } + + @Test + public void testPutStringArray_onlyNull_isEmpty() { + final String[] stringArray = new String[]{null}; + final String[] expectedStringArray = new String[]{}; + + TestEntity entity = new TestEntity(); + entity.setSimpleStringArray(stringArray); + box.put(entity); + + TestEntity entityRead = box.get(entity.getId()); + assertNotNull(entityRead); + assertArrayEquals(expectedStringArray, entityRead.getSimpleStringArray()); + } + @Test public void testPutGetUpdateGetRemove() { // create an entity From 994f91b6ca5675adb53daab7b072298535fdf0e2 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 4 Jun 2019 08:27:58 +0200 Subject: [PATCH 14/34] QueryTest: test contains matches string array item. --- .../src/test/java/io/objectbox/query/QueryTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest.java b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest.java index 68d43a84..a63c9336 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest.java @@ -47,6 +47,7 @@ import static io.objectbox.TestEntity_.simpleLong; import static io.objectbox.TestEntity_.simpleShort; import static io.objectbox.TestEntity_.simpleString; +import static io.objectbox.TestEntity_.simpleStringArray; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -243,6 +244,16 @@ public void testString() { assertEquals(2, box.query().contains(simpleString, "nana").build().count()); } + @Test + public void testStringArray() { + putTestEntitiesStrings(); + // contains(prop, value) matches if value is equal to one of the array items. + // Verify by not matching entity where 'banana' is only a substring of an array item ('banana milk shake'). + List results = box.query().contains(simpleStringArray, "banana").build().find(); + assertEquals(1, results.size()); + assertEquals("banana", results.get(0).getSimpleStringArray()[0]); + } + @Test public void testStringLess() { putTestEntitiesStrings(); From e1a8af0c07161b5fed88907a8c774f6cac0c7d8a Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 3 Mar 2020 07:50:41 +0100 Subject: [PATCH 15/34] Start development of version 3.0.0-alpha2. --- build.gradle | 6 ++++-- objectbox-java/src/main/java/io/objectbox/BoxStore.java | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 46e6ad66..0c840239 100644 --- a/build.gradle +++ b/build.gradle @@ -4,11 +4,13 @@ version = ob_version buildscript { ext { // Typically, only edit those two: - def objectboxVersionNumber = '2.5.2' // without "-SNAPSHOT", e.g. '2.5.0' or '2.4.0-RC' + def objectboxVersionNumber = '3.0.0-alpha2' // without "-SNAPSHOT", e.g. '2.5.0' or '2.4.0-RC' def objectboxVersionRelease = false // set to true for releasing to ignore versionPostFix to avoid e.g. "-dev" versions // version post fix: '-' or '' if not defined; e.g. used by CI to pass in branch name - def versionPostFixValue = project.findProperty('versionPostFix') +// def versionPostFixValue = project.findProperty('versionPostFix') + // PREVIEW RELEASE, do not use version post fix. + def versionPostFixValue = '' def versionPostFix = versionPostFixValue ? "-$versionPostFixValue" : '' ob_version = objectboxVersionNumber + (objectboxVersionRelease? "" : "$versionPostFix-SNAPSHOT") println "ObjectBox Java version $ob_version" diff --git a/objectbox-java/src/main/java/io/objectbox/BoxStore.java b/objectbox-java/src/main/java/io/objectbox/BoxStore.java index f75e3f8e..ac3889e6 100644 --- a/objectbox-java/src/main/java/io/objectbox/BoxStore.java +++ b/objectbox-java/src/main/java/io/objectbox/BoxStore.java @@ -66,7 +66,7 @@ public class BoxStore implements Closeable { /** Change so ReLinker will update native library when using workaround loading. */ public static final String JNI_VERSION = "2.5.1"; - private static final String VERSION = "2.5.2-2020-02-10"; + private static final String VERSION = "3.0.0-alpha2-2020-03-10"; private static BoxStore defaultStore; /** Currently used DB dirs with values from {@link #getCanonicalPath(File)}. */ From 350be2d34d62f58857cb364db2b12c2960df9fcb Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 3 Mar 2020 12:21:57 +0100 Subject: [PATCH 16/34] Use last stable version 2.5.1 of native libraries. --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0c840239..345c4e89 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,9 @@ buildscript { // Core version for tests // Be careful to diverge here; easy to forget and hard to find JNI problems - ob_native_version = objectboxVersionNumber + (objectboxVersionRelease? "": "-dev-SNAPSHOT") +// ob_native_version = objectboxVersionNumber + (objectboxVersionRelease? "": "-dev-SNAPSHOT") + // PREVIEW RELEASE, use last stable version 2.5.1 of native libraries. + ob_native_version = '2.5.1' def osName = System.getProperty("os.name").toLowerCase() objectboxPlatform = osName.contains('linux') ? 'linux' From fd9bae05213409806ad4784791454db7d79b4558 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 16 Mar 2020 08:35:41 +0100 Subject: [PATCH 17/34] Test parameter alias and combining with OR. https://github.com/objectbox/objectbox-java/issues/834 --- .../java/io/objectbox/query/QueryTest2.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest2.java b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest2.java index 35daaf7a..b70e9e4e 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest2.java +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTest2.java @@ -118,4 +118,24 @@ public void and() { assertEquals(2008, entities.get(0).getSimpleInt()); } + /** + * https://github.com/objectbox/objectbox-java/issues/834 + */ + @Test + public void parameterAlias_combineWithOr() { + putTestEntitiesScalars(); + + Query query = box.query( + simpleInt.greater(0).alias("greater") + .or(simpleInt.less(0).alias("less")) + ).order(simpleInt).build(); + List results = query + .setParameter("greater", 2008) + .setParameter("less", 2001) + .find(); + assertEquals(2, results.size()); + assertEquals(2000, results.get(0).getSimpleInt()); + assertEquals(2009, results.get(1).getSimpleInt()); + } + } From f9c6b2b4a1d46d0152061947e7a0abb7e8b6d724 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 9 Mar 2020 15:58:53 +0100 Subject: [PATCH 18/34] Add infix extension functions for Property condition methods. --- .../kotlin/io/objectbox/kotlin/Property.kt | 165 ++++++++++++++++++ .../java/io/objectbox/query/QueryTestK.kt | 27 ++- 2 files changed, 177 insertions(+), 15 deletions(-) create mode 100644 objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Property.kt diff --git a/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Property.kt b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Property.kt new file mode 100644 index 00000000..a339f01e --- /dev/null +++ b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Property.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2020 ObjectBox Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") // Public API. + +package io.objectbox.kotlin + +import io.objectbox.Property +import io.objectbox.query.PropertyQueryCondition +import java.util.* + + +// Boolean +/** Creates an "equal ('=')" condition for this property. */ +infix fun Property.equal(value: Boolean): PropertyQueryCondition { + return equal(value) +} + +/** Creates a "not equal ('<>')" condition for this property. */ +infix fun Property.notEqual(value: Boolean): PropertyQueryCondition { + return notEqual(value) +} + +// IntArray +/** Creates an "IN (..., ..., ...)" condition for this property. */ +infix fun Property.oneOf(value: IntArray): PropertyQueryCondition { + return oneOf(value) +} + +/** Creates a "NOT IN (..., ..., ...)" condition for this property. */ +infix fun Property.notOneOf(value: IntArray): PropertyQueryCondition { + return notOneOf(value) +} + +// Long +/** Creates an "equal ('=')" condition for this property. */ +infix fun Property.equal(value: Long): PropertyQueryCondition { + return equal(value) +} + +/** Creates a "not equal ('<>')" condition for this property. */ +infix fun Property.notEqual(value: Long): PropertyQueryCondition { + return notEqual(value) +} + +/** Creates a "greater than ('>')" condition for this property. */ +infix fun Property.greater(value: Long): PropertyQueryCondition { + return greater(value) +} + +/** Creates a "less than ('<')" condition for this property. */ +infix fun Property.less(value: Long): PropertyQueryCondition { + return less(value) +} + +// LongArray +/** Creates an "IN (..., ..., ...)" condition for this property. */ +infix fun Property.oneOf(value: LongArray): PropertyQueryCondition { + return oneOf(value) +} + +/** Creates a "NOT IN (..., ..., ...)" condition for this property. */ +infix fun Property.notOneOf(value: LongArray): PropertyQueryCondition { + return notOneOf(value) +} + +// Double +/** Creates a "greater than ('>')" condition for this property. */ +infix fun Property.greater(value: Double): PropertyQueryCondition { + return greater(value) +} + +/** Creates a "less than ('<')" condition for this property. */ +infix fun Property.less(value: Double): PropertyQueryCondition { + return less(value) +} + +// Date +/** Creates an "equal ('=')" condition for this property. */ +infix fun Property.equal(value: Date): PropertyQueryCondition { + return equal(value) +} + +/** Creates a "not equal ('<>')" condition for this property. */ +infix fun Property.notEqual(value: Date): PropertyQueryCondition { + return notEqual(value) +} + +/** Creates a "greater than ('>')" condition for this property. */ +infix fun Property.greater(value: Date): PropertyQueryCondition { + return greater(value) +} + +/** Creates a "less than ('<')" condition for this property. */ +infix fun Property.less(value: Date): PropertyQueryCondition { + return less(value) +} + +// String +/** Creates an "equal ('=')" condition for this property. */ +infix fun Property.equal(value: String): PropertyQueryCondition { + return equal(value) +} + +/** Creates a "not equal ('<>')" condition for this property. */ +infix fun Property.notEqual(value: String): PropertyQueryCondition { + return notEqual(value) +} + +/** Creates a "greater than ('>')" condition for this property. */ +infix fun Property.greater(value: String): PropertyQueryCondition { + return greater(value) +} + +/** Creates a "less than ('<')" condition for this property. */ +infix fun Property.less(value: String): PropertyQueryCondition { + return less(value) +} + +infix fun Property.contains(value: String): PropertyQueryCondition { + return contains(value) +} + +infix fun Property.startsWith(value: String): PropertyQueryCondition { + return startsWith(value) +} + +infix fun Property.endsWith(value: String): PropertyQueryCondition { + return endsWith(value) +} + +// Array +/** Creates an "IN (..., ..., ...)" condition for this property. */ +infix fun Property.oneOf(value: Array): PropertyQueryCondition { + return oneOf(value) +} + +// ByteArray +/** Creates an "equal ('=')" condition for this property. */ +infix fun Property.equal(value: ByteArray): PropertyQueryCondition { + return equal(value) +} + +/** Creates a "greater than ('>')" condition for this property. */ +infix fun Property.greater(value: ByteArray): PropertyQueryCondition { + return greater(value) +} + +/** Creates a "less than ('<')" condition for this property. */ +infix fun Property.less(value: ByteArray): PropertyQueryCondition { + return less(value) +} diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt index aad2555b..c5546d79 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt @@ -1,10 +1,7 @@ package io.objectbox.query import io.objectbox.TestEntity_ -import io.objectbox.kotlin.and -import io.objectbox.kotlin.inValues -import io.objectbox.kotlin.or -import io.objectbox.kotlin.query +import io.objectbox.kotlin.* import org.junit.Assert.assertEquals import org.junit.Test @@ -36,8 +33,8 @@ class QueryTestK : AbstractQueryTest() { // suggested query API val newQuery = box.query( - (TestEntity_.simpleInt.less(12) or TestEntity_.simpleLong.oneOf(longArrayOf(1012))) - and TestEntity_.simpleString.equal("Fry") + (TestEntity_.simpleInt less 12 or (TestEntity_.simpleLong oneOf longArrayOf(1012))) + and (TestEntity_.simpleString equal "Fry") ).order(TestEntity_.simpleInt).build() val resultsNew = newQuery.find() assertEquals(2, resultsNew.size) @@ -46,15 +43,15 @@ class QueryTestK : AbstractQueryTest() { val newQueryOr = box.query( // (EQ OR EQ) AND LESS - (TestEntity_.simpleString.equal("Fry") or TestEntity_.simpleString.equal("Sarah")) - and TestEntity_.simpleInt.less(12) + (TestEntity_.simpleString equal "Fry" or (TestEntity_.simpleString equal "Sarah")) + and (TestEntity_.simpleInt less 12) ).build().find() assertEquals(1, newQueryOr.size) // only the Fry age 10 val newQueryAnd = box.query( // EQ OR (EQ AND LESS) - TestEntity_.simpleString.equal("Fry") or - (TestEntity_.simpleString.equal("Sarah") and TestEntity_.simpleInt.less(12)) + TestEntity_.simpleString equal "Fry" or + (TestEntity_.simpleString equal "Sarah" and (TestEntity_.simpleInt less 12)) ).build().find() assertEquals(3, newQueryAnd.size) // all Fry's } @@ -63,8 +60,8 @@ class QueryTestK : AbstractQueryTest() { fun intLessAndGreater() { putTestEntitiesScalars() val query = box.query( - TestEntity_.simpleInt.greater(2003) - and TestEntity_.simpleShort.less(2107) + TestEntity_.simpleInt greater 2003 + and (TestEntity_.simpleShort less 2107) ).build() assertEquals(3, query.count()) } @@ -84,7 +81,7 @@ class QueryTestK : AbstractQueryTest() { val valuesInt = intArrayOf(1, 1, 2, 3, 2003, 2007, 2002, -1) val query = box.query( - TestEntity_.simpleInt.oneOf(valuesInt).alias("int") + (TestEntity_.simpleInt oneOf(valuesInt)).alias("int") ).build() assertEquals(3, query.count()) @@ -102,7 +99,7 @@ class QueryTestK : AbstractQueryTest() { fun or() { putTestEntitiesScalars() val query = box.query( - TestEntity_.simpleInt.equal(2007) or TestEntity_.simpleLong.equal(3002) + TestEntity_.simpleInt equal 2007 or (TestEntity_.simpleLong equal 3002) ).build() val entities = query.find() assertEquals(2, entities.size.toLong()) @@ -115,7 +112,7 @@ class QueryTestK : AbstractQueryTest() { putTestEntitiesScalars() // Result if OR precedence (wrong): {}, AND precedence (expected): {2008} val query = box.query( - TestEntity_.simpleInt.equal(2006) and TestEntity_.simpleInt.equal(2007) or TestEntity_.simpleInt.equal(2008) + TestEntity_.simpleInt equal 2006 and (TestEntity_.simpleInt equal 2007) or (TestEntity_.simpleInt equal 2008) ).build() val entities = query.find() assertEquals(1, entities.size.toLong()) From 773098e06e0f86cc146db5f3572f2b7f9f6e5b7f Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 16 Mar 2020 11:02:49 +0100 Subject: [PATCH 19/34] Move QueryCondition extensions functions to QueryCondition.kt file. --- .../kotlin/io/objectbox/kotlin/Extensions.kt | 19 ---------- .../io/objectbox/kotlin/QueryCondition.kt | 38 +++++++++++++++++++ 2 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/QueryCondition.kt diff --git a/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Extensions.kt b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Extensions.kt index e6880afc..41286bd8 100644 --- a/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Extensions.kt +++ b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Extensions.kt @@ -23,7 +23,6 @@ import io.objectbox.BoxStore import io.objectbox.Property import io.objectbox.query.Query import io.objectbox.query.QueryBuilder -import io.objectbox.query.QueryCondition import io.objectbox.relation.ToMany import kotlin.reflect.KClass @@ -156,21 +155,3 @@ inline fun ToMany.applyChangesToDb(resetFirst: Boolean = false, body: ToM body() applyChangesToDb() } - -/** - * Combines the left hand side condition using AND with the right hand side condition. - * - * @see or - */ -infix fun QueryCondition.and(queryCondition: QueryCondition): QueryCondition { - return and(queryCondition) -} - -/** - * Combines the left hand side condition using OR with the right hand side condition. - * - * @see and - */ -infix fun QueryCondition.or(queryCondition: QueryCondition): QueryCondition { - return or(queryCondition) -} diff --git a/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/QueryCondition.kt b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/QueryCondition.kt new file mode 100644 index 00000000..76e7c54d --- /dev/null +++ b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/QueryCondition.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020 ObjectBox Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.objectbox.kotlin + +import io.objectbox.query.QueryCondition + + +/** + * Combines the left hand side condition using AND with the right hand side condition. + * + * @see or + */ +infix fun QueryCondition.and(queryCondition: QueryCondition): QueryCondition { + return and(queryCondition) +} + +/** + * Combines the left hand side condition using OR with the right hand side condition. + * + * @see and + */ +infix fun QueryCondition.or(queryCondition: QueryCondition): QueryCondition { + return or(queryCondition) +} \ No newline at end of file From 84bbe4a0284514ff30b825aed6d99224cfc571ef Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 16 Mar 2020 11:14:13 +0100 Subject: [PATCH 20/34] Add infix function for PropertyQueryCondition.alias(name). --- .../kotlin/PropertyQueryCondition.kt | 28 +++++++++++++++++++ .../java/io/objectbox/query/QueryTestK.kt | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/PropertyQueryCondition.kt diff --git a/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/PropertyQueryCondition.kt b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/PropertyQueryCondition.kt new file mode 100644 index 00000000..a3791f3f --- /dev/null +++ b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/PropertyQueryCondition.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020 ObjectBox Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.objectbox.kotlin + +import io.objectbox.query.PropertyQueryCondition +import io.objectbox.query.QueryCondition + + +/** + * Assigns an alias to this condition that can later be used with the [io.objectbox.query.Query] setParameter methods. + */ +infix fun PropertyQueryCondition.alias(name: String): QueryCondition { + return alias(name) +} \ No newline at end of file diff --git a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt index c5546d79..9c802f8e 100644 --- a/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt +++ b/tests/objectbox-java-test/src/test/java/io/objectbox/query/QueryTestK.kt @@ -81,7 +81,7 @@ class QueryTestK : AbstractQueryTest() { val valuesInt = intArrayOf(1, 1, 2, 3, 2003, 2007, 2002, -1) val query = box.query( - (TestEntity_.simpleInt oneOf(valuesInt)).alias("int") + TestEntity_.simpleInt oneOf(valuesInt) alias "int" ).build() assertEquals(3, query.count()) From 897b9b441b8f3d1c3d499641c80999b294de6548 Mon Sep 17 00:00:00 2001 From: Markus Date: Mon, 30 Sep 2019 08:46:42 +0200 Subject: [PATCH 21/34] updates to FB properties model: ID_COMPANION flag, DateNano type --- .../main/java/io/objectbox/model/PropertyFlags.java | 8 ++++++++ .../main/java/io/objectbox/model/PropertyType.java | 13 ++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/objectbox-java/src/main/java/io/objectbox/model/PropertyFlags.java b/objectbox-java/src/main/java/io/objectbox/model/PropertyFlags.java index a7946a12..cd4d39dd 100644 --- a/objectbox-java/src/main/java/io/objectbox/model/PropertyFlags.java +++ b/objectbox-java/src/main/java/io/objectbox/model/PropertyFlags.java @@ -81,5 +81,13 @@ private PropertyFlags() { } * Note: Don't combine with ID (IDs are always unsigned internally). */ public static final int UNSIGNED = 8192; + /** + * By defining an ID companion property, the entity type uses a special ID encoding scheme involving this property + * in addition to the ID. + * + * For Time Series IDs, a companion property of type Date or DateNano represents the exact timestamp. + * (Future idea: string hash IDs, with a String companion property to store the full string ID). + */ + public static final int ID_COMPANION = 16384; } diff --git a/objectbox-java/src/main/java/io/objectbox/model/PropertyType.java b/objectbox-java/src/main/java/io/objectbox/model/PropertyType.java index 04f7baad..742c9852 100644 --- a/objectbox-java/src/main/java/io/objectbox/model/PropertyType.java +++ b/objectbox-java/src/main/java/io/objectbox/model/PropertyType.java @@ -18,6 +18,9 @@ package io.objectbox.model; +/** + * Basic type of a property + */ public final class PropertyType { private PropertyType() { } /** @@ -34,14 +37,17 @@ private PropertyType() { } public static final short Double = 8; public static final short String = 9; /** - * Internally stored as a 64 bit long(?) + * Date/time stored as a 64 bit long representing milliseconds since 1970-01-01 (unix epoch) */ public static final short Date = 10; /** * Relation to another entity */ public static final short Relation = 11; - public static final short Reserved1 = 12; + /** + * High precision date/time stored as a 64 bit long representing nanoseconds since 1970-01-01 (unix epoch) + */ + public static final short DateNano = 12; public static final short Reserved2 = 13; public static final short Reserved3 = 14; public static final short Reserved4 = 15; @@ -61,8 +67,9 @@ private PropertyType() { } public static final short DoubleVector = 29; public static final short StringVector = 30; public static final short DateVector = 31; + public static final short DateNanoVector = 32; - public static final String[] names = { "Unknown", "Bool", "Byte", "Short", "Char", "Int", "Long", "Float", "Double", "String", "Date", "Relation", "Reserved1", "Reserved2", "Reserved3", "Reserved4", "Reserved5", "Reserved6", "Reserved7", "Reserved8", "Reserved9", "Reserved10", "BoolVector", "ByteVector", "ShortVector", "CharVector", "IntVector", "LongVector", "FloatVector", "DoubleVector", "StringVector", "DateVector", }; + public static final String[] names = { "Unknown", "Bool", "Byte", "Short", "Char", "Int", "Long", "Float", "Double", "String", "Date", "Relation", "DateNano", "Reserved2", "Reserved3", "Reserved4", "Reserved5", "Reserved6", "Reserved7", "Reserved8", "Reserved9", "Reserved10", "BoolVector", "ByteVector", "ShortVector", "CharVector", "IntVector", "LongVector", "FloatVector", "DoubleVector", "StringVector", "DateVector", "DateNanoVector", }; public static String name(int e) { return names[e]; } } From 3b20e876fbd5202b12a8a713f25602e7ce480a32 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 1 Oct 2019 13:06:22 +0200 Subject: [PATCH 22/34] Add @Type annotation and DatabaseType enum. --- .../io/objectbox/annotation/DatabaseType.java | 16 +++++++++ .../java/io/objectbox/annotation/Type.java | 33 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 objectbox-java-api/src/main/java/io/objectbox/annotation/DatabaseType.java create mode 100644 objectbox-java-api/src/main/java/io/objectbox/annotation/Type.java diff --git a/objectbox-java-api/src/main/java/io/objectbox/annotation/DatabaseType.java b/objectbox-java-api/src/main/java/io/objectbox/annotation/DatabaseType.java new file mode 100644 index 00000000..dcd24694 --- /dev/null +++ b/objectbox-java-api/src/main/java/io/objectbox/annotation/DatabaseType.java @@ -0,0 +1,16 @@ +package io.objectbox.annotation; + +/** + * Use with {@link Type @Type} to specify how a property value is stored in the database. + *

    + * This is e.g. useful for integer types that can mean different things depending on interpretation. + * For example a 64-bit long value might be interpreted as time in milliseconds, or as time in nanoseconds. + */ +public enum DatabaseType { + + /** + * High precision time stored as a 64-bit long representing nanoseconds since 1970-01-01 (unix epoch). + */ + DateNano + +} diff --git a/objectbox-java-api/src/main/java/io/objectbox/annotation/Type.java b/objectbox-java-api/src/main/java/io/objectbox/annotation/Type.java new file mode 100644 index 00000000..e9fa3d1f --- /dev/null +++ b/objectbox-java-api/src/main/java/io/objectbox/annotation/Type.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 ObjectBox Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.objectbox.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies the database type of an annotated property as one of {@link DatabaseType}. + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.FIELD}) +public @interface Type { + + DatabaseType value(); + +} From c1afae1779b13b51d968eba59c7e89213373f812 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 23 Mar 2020 14:00:31 +0100 Subject: [PATCH 23/34] Document how to use case sensitive conditions for String. Also recommend using case sensitive conditions for indexed strings. --- .../java/io/objectbox/query/QueryBuilder.java | 70 ++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/objectbox-java/src/main/java/io/objectbox/query/QueryBuilder.java b/objectbox-java/src/main/java/io/objectbox/query/QueryBuilder.java index 2d24afaf..a9b25b1c 100644 --- a/objectbox-java/src/main/java/io/objectbox/query/QueryBuilder.java +++ b/objectbox-java/src/main/java/io/objectbox/query/QueryBuilder.java @@ -22,6 +22,8 @@ import java.util.Date; import java.util.List; +import javax.annotation.Nullable; + import io.objectbox.Box; import io.objectbox.EntityInfo; import io.objectbox.Property; @@ -29,8 +31,6 @@ import io.objectbox.annotation.apihint.Internal; import io.objectbox.relation.RelationInfo; -import javax.annotation.Nullable; - /** * With QueryBuilder you define custom queries returning matching entities. Using the methods of this class you can * select (filter) results for specific data (for example #{@link #equal(Property, String)} and @@ -647,42 +647,96 @@ public QueryBuilder notIn(Property property, int[] values) { // String /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /** + * Creates an "equal ('=')" condition for this property. + *

    + * Ignores case when matching results, e.g. {@code equal(prop, "example")} matches both "Example" and "example". + *

    + * Use {@link #equal(Property, String, StringOrder) equal(prop, value, StringOrder.CASE_SENSITIVE)} to only match + * if case is equal. + *

    + * Note: Use a case sensitive condition to utilize an {@link io.objectbox.annotation.Index @Index} + * on {@code property}, dramatically speeding up look-up of results. + */ public QueryBuilder equal(Property property, String value) { verifyHandle(); checkCombineCondition(nativeEqual(handle, property.getId(), value, false)); return this; } + /** + * Creates an "equal ('=')" condition for this property. + *

    + * Set {@code order} to {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to only match + * if case is equal. E.g. {@code equal(prop, "example", StringOrder.CASE_SENSITIVE)} only matches "example", + * but not "Example". + *

    + * Note: Use a case sensitive condition to utilize an {@link io.objectbox.annotation.Index @Index} + * on {@code property}, dramatically speeding up look-up of results. + */ public QueryBuilder equal(Property property, String value, StringOrder order) { verifyHandle(); checkCombineCondition(nativeEqual(handle, property.getId(), value, order == StringOrder.CASE_SENSITIVE)); return this; } + /** + * Creates a "not equal ('<>')" condition for this property. + *

    + * Ignores case when matching results, e.g. {@code notEqual(prop, "example")} excludes both "Example" and "example". + *

    + * Use {@link #notEqual(Property, String, StringOrder) notEqual(prop, value, StringOrder.CASE_SENSITIVE)} to only exclude + * if case is equal. + *

    + * Note: Use a case sensitive condition to utilize an {@link io.objectbox.annotation.Index @Index} + * on {@code property}, dramatically speeding up look-up of results. + */ public QueryBuilder notEqual(Property property, String value) { verifyHandle(); checkCombineCondition(nativeNotEqual(handle, property.getId(), value, false)); return this; } + /** + * Creates a "not equal ('<>')" condition for this property. + *

    + * Set {@code order} to {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to only exclude + * if case is equal. E.g. {@code notEqual(prop, "example", StringOrder.CASE_SENSITIVE)} only excludes "example", + * but not "Example". + *

    + * Note: Use a case sensitive condition to utilize an {@link io.objectbox.annotation.Index @Index} + * on {@code property}, dramatically speeding up look-up of results. + */ public QueryBuilder notEqual(Property property, String value, StringOrder order) { verifyHandle(); checkCombineCondition(nativeNotEqual(handle, property.getId(), value, order == StringOrder.CASE_SENSITIVE)); return this; } + /** + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + */ public QueryBuilder contains(Property property, String value) { verifyHandle(); checkCombineCondition(nativeContains(handle, property.getId(), value, false)); return this; } + /** + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + */ public QueryBuilder startsWith(Property property, String value) { verifyHandle(); checkCombineCondition(nativeStartsWith(handle, property.getId(), value, false)); return this; } + /** + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + */ public QueryBuilder endsWith(Property property, String value) { verifyHandle(); checkCombineCondition(nativeEndsWith(handle, property.getId(), value, false)); @@ -707,6 +761,10 @@ public QueryBuilder endsWith(Property property, String value, StringOrder return this; } + /** + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + */ public QueryBuilder less(Property property, String value) { return less(property, value, StringOrder.CASE_INSENSITIVE); } @@ -717,6 +775,10 @@ public QueryBuilder less(Property property, String value, StringOrder orde return this; } + /** + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + */ public QueryBuilder greater(Property property, String value) { return greater(property, value, StringOrder.CASE_INSENSITIVE); } @@ -727,6 +789,10 @@ public QueryBuilder greater(Property property, String value, StringOrder o return this; } + /** + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + */ public QueryBuilder in(Property property, String[] values) { return in(property, values, StringOrder.CASE_INSENSITIVE); } From a3aea9a0ee3b152cbe484dfcacc96495b2547efd Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 23 Mar 2020 14:18:55 +0100 Subject: [PATCH 24/34] Document how to use case sensitive conditions for String properties. --- .../src/main/java/io/objectbox/Property.java | 106 ++++++++++++++++-- 1 file changed, 96 insertions(+), 10 deletions(-) diff --git a/objectbox-java/src/main/java/io/objectbox/Property.java b/objectbox-java/src/main/java/io/objectbox/Property.java index 853b3ec0..d09860c5 100644 --- a/objectbox-java/src/main/java/io/objectbox/Property.java +++ b/objectbox-java/src/main/java/io/objectbox/Property.java @@ -215,46 +215,111 @@ public PropertyQueryCondition between(Date lowerBoundary, Date upperBoun return new LongLongCondition<>(this, LongLongCondition.Operation.BETWEEN, lowerBoundary, upperBoundary); } - /** Creates an "equal ('=')" condition for this property. */ + /** + * Creates an "equal ('=')" condition for this property. + *

    + * Ignores case when matching results, e.g. {@code equal("example")} matches both "Example" and "example". + *

    + * Use {@link #equal(String, StringOrder) equal(value, StringOrder.CASE_SENSITIVE)} to only match if case is equal. + *

    + * Note: Use a case sensitive condition to utilize an {@link io.objectbox.annotation.Index @Index} + * on {@code property}, dramatically speeding up look-up of results. + * + * @see #equal(String, StringOrder) + */ public PropertyQueryCondition equal(String value) { return new StringCondition<>(this, StringCondition.Operation.EQUAL, value); } - /** Creates an "equal ('=')" condition for this property. */ + /** + * Creates an "equal ('=')" condition for this property. + *

    + * Set {@code order} to {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to only match + * if case is equal. E.g. {@code equal("example", StringOrder.CASE_SENSITIVE)} only matches "example", + * but not "Example". + *

    + * Note: Use a case sensitive condition to utilize an {@link io.objectbox.annotation.Index @Index} + * on {@code property}, dramatically speeding up look-up of results. + */ public PropertyQueryCondition equal(String value, StringOrder order) { return new StringCondition<>(this, StringCondition.Operation.EQUAL, value, order); } - /** Creates a "not equal ('<>')" condition for this property. */ + /** + * Creates a "not equal ('<>')" condition for this property. + *

    + * Ignores case when matching results, e.g. {@code notEqual("example")} excludes both "Example" and "example". + *

    + * Use {@link #notEqual(String, StringOrder) notEqual(value, StringOrder.CASE_SENSITIVE)} to only exclude + * if case is equal. + *

    + * Note: Use a case sensitive condition to utilize an {@link io.objectbox.annotation.Index @Index} + * on {@code property}, dramatically speeding up look-up of results. + * + * @see #notEqual(String, StringOrder) + */ public PropertyQueryCondition notEqual(String value) { return new StringCondition<>(this, StringCondition.Operation.NOT_EQUAL, value); } - /** Creates a "not equal ('<>')" condition for this property. */ + /** + * Creates a "not equal ('<>')" condition for this property. + *

    + * Set {@code order} to {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to only exclude + * if case is equal. E.g. {@code notEqual("example", StringOrder.CASE_SENSITIVE)} only excludes "example", + * but not "Example". + *

    + * Note: Use a case sensitive condition to utilize an {@link io.objectbox.annotation.Index @Index} + * on {@code property}, dramatically speeding up look-up of results. + */ public PropertyQueryCondition notEqual(String value, StringOrder order) { return new StringCondition<>(this, StringCondition.Operation.NOT_EQUAL, value, order); } - /** Creates a "greater than ('>')" condition for this property. */ + /** + * Creates a "greater than ('>')" condition for this property. + *

    + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + * + * @see #greater(String, StringOrder) + */ public PropertyQueryCondition greater(String value) { return new StringCondition<>(this, StringCondition.Operation.GREATER, value); } - /** Creates a "greater than ('>')" condition for this property. */ + /** + * Creates a "greater than ('>')" condition for this property. + */ public PropertyQueryCondition greater(String value, StringOrder order) { return new StringCondition<>(this, StringCondition.Operation.GREATER, value, order); } - /** Creates a "less than ('<')" condition for this property. */ + /** + * Creates a "less than ('<')" condition for this property. + *

    + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + * + * @see #less(String, StringOrder) + */ public PropertyQueryCondition less(String value) { return new StringCondition<>(this, StringCondition.Operation.LESS, value); } - /** Creates a "less than ('<')" condition for this property. */ + /** + * Creates a "less than ('<')" condition for this property. + */ public PropertyQueryCondition less(String value, StringOrder order) { return new StringCondition<>(this, StringCondition.Operation.LESS, value, order); } + /** + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + * + * @see #contains(String, StringOrder) + */ public PropertyQueryCondition contains(String value) { return new StringCondition<>(this, StringCondition.Operation.CONTAINS, value); } @@ -263,6 +328,12 @@ public PropertyQueryCondition contains(String value, StringOrder order) return new StringCondition<>(this, StringCondition.Operation.CONTAINS, value, order); } + /** + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + * + * @see #startsWith(String, StringOrder) + */ public PropertyQueryCondition startsWith(String value) { return new StringCondition<>(this, Operation.STARTS_WITH, value); } @@ -271,6 +342,12 @@ public PropertyQueryCondition startsWith(String value, StringOrder order return new StringCondition<>(this, Operation.STARTS_WITH, value, order); } + /** + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + * + * @see #endsWith(String, StringOrder) + */ public PropertyQueryCondition endsWith(String value) { return new StringCondition<>(this, Operation.ENDS_WITH, value); } @@ -279,12 +356,21 @@ public PropertyQueryCondition endsWith(String value, StringOrder order) return new StringCondition<>(this, Operation.ENDS_WITH, value, order); } - /** Creates an "IN (..., ..., ...)" condition for this property. */ + /** + * Creates an "IN (..., ..., ...)" condition for this property. + *

    + * Ignores case when matching results. Use the overload and pass + * {@link StringOrder#CASE_SENSITIVE StringOrder.CASE_SENSITIVE} to specify that case should not be ignored. + * + * @see #oneOf(String[], StringOrder) + */ public PropertyQueryCondition oneOf(String[] values) { return new StringArrayCondition<>(this, StringArrayCondition.Operation.IN, values); } - /** Creates an "IN (..., ..., ...)" condition for this property. */ + /** + * Creates an "IN (..., ..., ...)" condition for this property. + */ public PropertyQueryCondition oneOf(String[] values, StringOrder order) { return new StringArrayCondition<>(this, StringArrayCondition.Operation.IN, values, order); } From 6283b2d88e8931b3394459a9625f71384e906b6b Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 23 Mar 2020 14:39:27 +0100 Subject: [PATCH 25/34] Start RxJava 3 support by copying RxJava 2 module. --- build.gradle | 3 +- objectbox-rxjava3/README.md | 31 ++++ objectbox-rxjava3/build.gradle | 51 +++++ .../main/java/io/objectbox/rx/RxBoxStore.java | 57 ++++++ .../main/java/io/objectbox/rx/RxQuery.java | 132 +++++++++++++ .../objectbox/query/FakeQueryPublisher.java | 62 +++++++ .../java/io/objectbox/query/MockQuery.java | 57 ++++++ .../io/objectbox/rx/QueryObserverTest.java | 175 ++++++++++++++++++ settings.gradle | 1 + 9 files changed, 568 insertions(+), 1 deletion(-) create mode 100644 objectbox-rxjava3/README.md create mode 100644 objectbox-rxjava3/build.gradle create mode 100644 objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java create mode 100644 objectbox-rxjava3/src/main/java/io/objectbox/rx/RxQuery.java create mode 100644 objectbox-rxjava3/src/test/java/io/objectbox/query/FakeQueryPublisher.java create mode 100644 objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java create mode 100644 objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java diff --git a/build.gradle b/build.gradle index 46e6ad66..37780c34 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,8 @@ def projectNamesToPublish = [ 'objectbox-java-api', 'objectbox-java', 'objectbox-kotlin', - 'objectbox-rxjava' + 'objectbox-rxjava', + 'objectbox-rxjava3' ] configure(subprojects.findAll { projectNamesToPublish.contains(it.name) }) { diff --git a/objectbox-rxjava3/README.md b/objectbox-rxjava3/README.md new file mode 100644 index 00000000..bfc79769 --- /dev/null +++ b/objectbox-rxjava3/README.md @@ -0,0 +1,31 @@ +RxJava 3 APIs for ObjectBox +=========================== +While ObjectBox has [data observers and reactive extensions](https://docs.objectbox.io/data-observers-and-rx) built-in, +this project adds RxJava 3 support. + +For general object changes, you can use `RxBoxStore` to create an `Observable`. + +`RxQuery` allows you to interact with ObjectBox `Query` objects using: + * Flowable + * Observable + * Single + +For example to get query results and subscribe to future updates (Object changes will automatically emmit new data): + +```java +Query query = box.query().build(); +RxQuery.observable(query).subscribe(this); +``` + +Adding the library to your project +----------------- +Grab via Gradle: +```gradle +implementation "io.objectbox:objectbox-rxjava3:$objectboxVersion" +``` + +Links +----- +[Data Observers and Rx Documentation](https://docs.objectbox.io/data-observers-and-rx) + +[Note App example](https://github.com/objectbox/objectbox-examples/blob/master/objectbox-example/src/main/java/io/objectbox/example/ReactiveNoteActivity.java) diff --git a/objectbox-rxjava3/build.gradle b/objectbox-rxjava3/build.gradle new file mode 100644 index 00000000..1c82a18d --- /dev/null +++ b/objectbox-rxjava3/build.gradle @@ -0,0 +1,51 @@ +apply plugin: 'java' + +group = 'io.objectbox' +version= rootProject.version + +sourceCompatibility = 1.8 + +dependencies { + compile project(':objectbox-java') + compile 'io.reactivex.rxjava2:rxjava:2.2.18' + + testCompile "junit:junit:$junit_version" + // Mockito 3.x requires Java 8. + testCompile 'org.mockito:mockito-core:2.28.2' +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from 'build/docs/javadoc' +} + +task sourcesJar(type: Jar) { + from sourceSets.main.allSource + classifier = 'sources' +} + +artifacts { + // java plugin adds jar. + archives javadocJar + archives sourcesJar +} + +uploadArchives { + repositories { + mavenDeployer { + // Basic definitions are defined in root project + pom.project { + name 'ObjectBox RxJava 3 API' + description 'RxJava 3 extensions for ObjectBox' + + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + } + } + } +} diff --git a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java b/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java new file mode 100644 index 00000000..8ffcbbc3 --- /dev/null +++ b/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017 ObjectBox Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.objectbox.rx; + +import io.objectbox.BoxStore; +import io.objectbox.reactive.DataObserver; +import io.objectbox.reactive.DataSubscription; +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.functions.Cancellable; + +/** + * Static methods to Rx-ify ObjectBox queries. + */ +public abstract class RxBoxStore { + /** + * Using the returned Observable, you can be notified about data changes. + * Once a transaction is committed, you will get info on classes with changed Objects. + */ + public static Observable observable(final BoxStore boxStore) { + return Observable.create(new ObservableOnSubscribe() { + @Override + public void subscribe(final ObservableEmitter emitter) throws Exception { + final DataSubscription dataSubscription = boxStore.subscribe().observer(new DataObserver() { + @Override + public void onData(Class data) { + if (!emitter.isDisposed()) { + emitter.onNext(data); + } + } + }); + emitter.setCancellable(new Cancellable() { + @Override + public void cancel() throws Exception { + dataSubscription.cancel(); + } + }); + } + }); + } + +} diff --git a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxQuery.java b/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxQuery.java new file mode 100644 index 00000000..1fef9d10 --- /dev/null +++ b/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxQuery.java @@ -0,0 +1,132 @@ +/* + * Copyright 2017 ObjectBox Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.objectbox.rx; + +import java.util.List; + +import io.objectbox.query.Query; +import io.objectbox.reactive.DataObserver; +import io.objectbox.reactive.DataSubscription; +import io.reactivex.BackpressureStrategy; +import io.reactivex.Flowable; +import io.reactivex.FlowableEmitter; +import io.reactivex.FlowableOnSubscribe; +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.Single; +import io.reactivex.SingleEmitter; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.functions.Cancellable; + +/** + * Static methods to Rx-ify ObjectBox queries. + */ +public abstract class RxQuery { + /** + * The returned Flowable emits Query results one by one. Once all results have been processed, onComplete is called. + * Uses BackpressureStrategy.BUFFER. + */ + public static Flowable flowableOneByOne(final Query query) { + return flowableOneByOne(query, BackpressureStrategy.BUFFER); + } + + /** + * The returned Flowable emits Query results one by one. Once all results have been processed, onComplete is called. + * Uses given BackpressureStrategy. + */ + public static Flowable flowableOneByOne(final Query query, BackpressureStrategy strategy) { + return Flowable.create(new FlowableOnSubscribe() { + @Override + public void subscribe(final FlowableEmitter emitter) throws Exception { + createListItemEmitter(query, emitter); + } + + }, strategy); + } + + static void createListItemEmitter(final Query query, final FlowableEmitter emitter) { + final DataSubscription dataSubscription = query.subscribe().observer(new DataObserver>() { + @Override + public void onData(List data) { + for (T datum : data) { + if (emitter.isCancelled()) { + return; + } else { + emitter.onNext(datum); + } + } + if (!emitter.isCancelled()) { + emitter.onComplete(); + } + } + }); + emitter.setCancellable(new Cancellable() { + @Override + public void cancel() throws Exception { + dataSubscription.cancel(); + } + }); + } + + /** + * The returned Observable emits Query results as Lists. + * Never completes, so you will get updates when underlying data changes + * (see {@link Query#subscribe()} for details). + */ + public static Observable> observable(final Query query) { + return Observable.create(new ObservableOnSubscribe>() { + @Override + public void subscribe(final ObservableEmitter> emitter) throws Exception { + final DataSubscription dataSubscription = query.subscribe().observer(new DataObserver>() { + @Override + public void onData(List data) { + if (!emitter.isDisposed()) { + emitter.onNext(data); + } + } + }); + emitter.setCancellable(new Cancellable() { + @Override + public void cancel() throws Exception { + dataSubscription.cancel(); + } + }); + } + }); + } + + /** + * The returned Single emits one Query result as a List. + */ + public static Single> single(final Query query) { + return Single.create(new SingleOnSubscribe>() { + @Override + public void subscribe(final SingleEmitter> emitter) throws Exception { + query.subscribe().single().observer(new DataObserver>() { + @Override + public void onData(List data) { + if (!emitter.isDisposed()) { + emitter.onSuccess(data); + } + } + }); + // no need to cancel, single never subscribes + } + }); + } +} diff --git a/objectbox-rxjava3/src/test/java/io/objectbox/query/FakeQueryPublisher.java b/objectbox-rxjava3/src/test/java/io/objectbox/query/FakeQueryPublisher.java new file mode 100644 index 00000000..6237c75a --- /dev/null +++ b/objectbox-rxjava3/src/test/java/io/objectbox/query/FakeQueryPublisher.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017 ObjectBox Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.objectbox.query; + +import io.objectbox.reactive.DataObserver; +import io.objectbox.reactive.DataPublisher; +import io.objectbox.reactive.DataPublisherUtils; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +public class FakeQueryPublisher implements DataPublisher> { + + private final Set>> observers = new CopyOnWriteArraySet(); + + private List queryResult = Collections.emptyList(); + + public List getQueryResult() { + return queryResult; + } + + public void setQueryResult(List queryResult) { + this.queryResult = queryResult; + } + + @Override + public synchronized void subscribe(DataObserver> observer, Object param) { + observers.add(observer); + } + + @Override + public void publishSingle(final DataObserver> observer, Object param) { + observer.onData(queryResult); + } + + public void publish() { + for (DataObserver> observer : observers) { + observer.onData(queryResult); + } + } + + @Override + public synchronized void unsubscribe(DataObserver> observer, Object param) { + DataPublisherUtils.removeObserverFromCopyOnWriteSet(observers, observer); + } + +} diff --git a/objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java b/objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java new file mode 100644 index 00000000..3e86e7c7 --- /dev/null +++ b/objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017 ObjectBox Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.objectbox.query; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.objectbox.Box; +import io.objectbox.BoxStore; +import io.objectbox.reactive.SubscriptionBuilder; + +public class MockQuery { + private Box box; + private BoxStore boxStore; + private final Query query; + private final FakeQueryPublisher fakeQueryPublisher; + + public MockQuery(boolean hasOrder) { + // box = mock(Box.class); + // boxStore = mock(BoxStore.class); + // when(box.getStore()).thenReturn(boxStore); + query = mock(Query.class); + fakeQueryPublisher = new FakeQueryPublisher(); + SubscriptionBuilder subscriptionBuilder = new SubscriptionBuilder(fakeQueryPublisher, null, null); + when(query.subscribe()).thenReturn(subscriptionBuilder); + } + + public Box getBox() { + return box; + } + + public BoxStore getBoxStore() { + return boxStore; + } + + public Query getQuery() { + return query; + } + + public FakeQueryPublisher getFakeQueryPublisher() { + return fakeQueryPublisher; + } +} diff --git a/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java b/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java new file mode 100644 index 00000000..7389effd --- /dev/null +++ b/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java @@ -0,0 +1,175 @@ +/* + * Copyright 2017 ObjectBox Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.objectbox.rx; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import io.objectbox.query.FakeQueryPublisher; +import io.objectbox.query.MockQuery; +import io.reactivex.Flowable; +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.Single; +import io.reactivex.SingleObserver; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; + +import static org.junit.Assert.*; + +@RunWith(MockitoJUnitRunner.class) +public class QueryObserverTest implements Observer>, SingleObserver>, Consumer { + + private List> receivedChanges = new CopyOnWriteArrayList<>(); + private CountDownLatch latch = new CountDownLatch(1); + + private MockQuery mockQuery = new MockQuery<>(false); + private FakeQueryPublisher publisher = mockQuery.getFakeQueryPublisher(); + private List listResult = new ArrayList<>(); + private Throwable error; + + private AtomicInteger completedCount = new AtomicInteger(); + + @Before + public void prep() { + listResult.add("foo"); + listResult.add("bar"); + } + + @Test + public void testObservable() { + Observable observable = RxQuery.observable(mockQuery.getQuery()); + observable.subscribe((Observer) this); + assertLatchCountedDown(latch, 2); + assertEquals(1, receivedChanges.size()); + assertEquals(0, receivedChanges.get(0).size()); + assertNull(error); + + latch = new CountDownLatch(1); + receivedChanges.clear(); + publisher.setQueryResult(listResult); + publisher.publish(); + + assertLatchCountedDown(latch, 5); + assertEquals(1, receivedChanges.size()); + assertEquals(2, receivedChanges.get(0).size()); + + assertEquals(0, completedCount.get()); + + //Unsubscribe? + // receivedChanges.clear(); + // latch = new CountDownLatch(1); + // assertLatchCountedDown(latch, 5); + // + // assertEquals(1, receivedChanges.size()); + // assertEquals(3, receivedChanges.get(0).size()); + } + + @Test + public void testFlowableOneByOne() { + publisher.setQueryResult(listResult); + + latch = new CountDownLatch(2); + Flowable flowable = RxQuery.flowableOneByOne(mockQuery.getQuery()); + flowable.subscribe(this); + assertLatchCountedDown(latch, 2); + assertEquals(2, receivedChanges.size()); + assertEquals(1, receivedChanges.get(0).size()); + assertEquals(1, receivedChanges.get(1).size()); + assertNull(error); + + receivedChanges.clear(); + publisher.publish(); + assertNoMoreResults(); + } + + @Test + public void testSingle() { + publisher.setQueryResult(listResult); + Single single = RxQuery.single(mockQuery.getQuery()); + single.subscribe((SingleObserver) this); + assertLatchCountedDown(latch, 2); + assertEquals(1, receivedChanges.size()); + assertEquals(2, receivedChanges.get(0).size()); + + receivedChanges.clear(); + publisher.publish(); + assertNoMoreResults(); + } + + protected void assertNoMoreResults() { + assertEquals(0, receivedChanges.size()); + try { + Thread.sleep(20); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + assertEquals(0, receivedChanges.size()); + } + + protected void assertLatchCountedDown(CountDownLatch latch, int seconds) { + try { + assertTrue(latch.await(seconds, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(List queryResult) { + receivedChanges.add(queryResult); + latch.countDown(); + } + + @Override + public void onNext(List queryResult) { + receivedChanges.add(queryResult); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + error = e; + } + + @Override + public void onComplete() { + completedCount.incrementAndGet(); + } + + @Override + public void accept(@NonNull String s) throws Exception { + receivedChanges.add(Collections.singletonList(s)); + latch.countDown(); + } +} diff --git a/settings.gradle b/settings.gradle index 35d2596a..24da1d92 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,7 @@ include ':objectbox-java-api' include ':objectbox-java' include ':objectbox-kotlin' include ':objectbox-rxjava' +include ':objectbox-rxjava3' include ':tests:objectbox-java-test' include ':tests:test-proguard' From 9a28f41b59d24237aa35b351ff193956cb2d6f3a Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 23 Mar 2020 14:53:10 +0100 Subject: [PATCH 26/34] Migrate to RxJava 3, update mockito [2.28.2->3.3.3] as Java 8 now available. --- objectbox-rxjava3/build.gradle | 5 +- .../main/java/io/objectbox/rx/RxBoxStore.java | 31 ++---- .../main/java/io/objectbox/rx/RxQuery.java | 96 ++++++------------- .../io/objectbox/rx/QueryObserverTest.java | 17 ++-- 4 files changed, 48 insertions(+), 101 deletions(-) diff --git a/objectbox-rxjava3/build.gradle b/objectbox-rxjava3/build.gradle index 1c82a18d..cae8854f 100644 --- a/objectbox-rxjava3/build.gradle +++ b/objectbox-rxjava3/build.gradle @@ -7,11 +7,10 @@ sourceCompatibility = 1.8 dependencies { compile project(':objectbox-java') - compile 'io.reactivex.rxjava2:rxjava:2.2.18' + compile 'io.reactivex.rxjava3:rxjava:3.0.1' testCompile "junit:junit:$junit_version" - // Mockito 3.x requires Java 8. - testCompile 'org.mockito:mockito-core:2.28.2' + testCompile 'org.mockito:mockito-core:3.3.3' } task javadocJar(type: Jar, dependsOn: javadoc) { diff --git a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java b/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java index 8ffcbbc3..c1117abc 100644 --- a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java +++ b/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java @@ -17,12 +17,8 @@ package io.objectbox.rx; import io.objectbox.BoxStore; -import io.objectbox.reactive.DataObserver; import io.objectbox.reactive.DataSubscription; -import io.reactivex.Observable; -import io.reactivex.ObservableEmitter; -import io.reactivex.ObservableOnSubscribe; -import io.reactivex.functions.Cancellable; +import io.reactivex.rxjava3.core.Observable; /** * Static methods to Rx-ify ObjectBox queries. @@ -33,24 +29,13 @@ public abstract class RxBoxStore { * Once a transaction is committed, you will get info on classes with changed Objects. */ public static Observable observable(final BoxStore boxStore) { - return Observable.create(new ObservableOnSubscribe() { - @Override - public void subscribe(final ObservableEmitter emitter) throws Exception { - final DataSubscription dataSubscription = boxStore.subscribe().observer(new DataObserver() { - @Override - public void onData(Class data) { - if (!emitter.isDisposed()) { - emitter.onNext(data); - } - } - }); - emitter.setCancellable(new Cancellable() { - @Override - public void cancel() throws Exception { - dataSubscription.cancel(); - } - }); - } + return Observable.create(emitter -> { + final DataSubscription dataSubscription = boxStore.subscribe().observer(data -> { + if (!emitter.isDisposed()) { + emitter.onNext(data); + } + }); + emitter.setCancellable(dataSubscription::cancel); }); } diff --git a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxQuery.java b/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxQuery.java index 1fef9d10..ad278d31 100644 --- a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxQuery.java +++ b/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxQuery.java @@ -19,19 +19,12 @@ import java.util.List; import io.objectbox.query.Query; -import io.objectbox.reactive.DataObserver; import io.objectbox.reactive.DataSubscription; -import io.reactivex.BackpressureStrategy; -import io.reactivex.Flowable; -import io.reactivex.FlowableEmitter; -import io.reactivex.FlowableOnSubscribe; -import io.reactivex.Observable; -import io.reactivex.ObservableEmitter; -import io.reactivex.ObservableOnSubscribe; -import io.reactivex.Single; -import io.reactivex.SingleEmitter; -import io.reactivex.SingleOnSubscribe; -import io.reactivex.functions.Cancellable; +import io.reactivex.rxjava3.core.BackpressureStrategy; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.FlowableEmitter; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; /** * Static methods to Rx-ify ObjectBox queries. @@ -50,37 +43,23 @@ public static Flowable flowableOneByOne(final Query query) { * Uses given BackpressureStrategy. */ public static Flowable flowableOneByOne(final Query query, BackpressureStrategy strategy) { - return Flowable.create(new FlowableOnSubscribe() { - @Override - public void subscribe(final FlowableEmitter emitter) throws Exception { - createListItemEmitter(query, emitter); - } - - }, strategy); + return Flowable.create(emitter -> createListItemEmitter(query, emitter), strategy); } static void createListItemEmitter(final Query query, final FlowableEmitter emitter) { - final DataSubscription dataSubscription = query.subscribe().observer(new DataObserver>() { - @Override - public void onData(List data) { - for (T datum : data) { - if (emitter.isCancelled()) { - return; - } else { - emitter.onNext(datum); - } - } - if (!emitter.isCancelled()) { - emitter.onComplete(); + final DataSubscription dataSubscription = query.subscribe().observer(data -> { + for (T datum : data) { + if (emitter.isCancelled()) { + return; + } else { + emitter.onNext(datum); } } - }); - emitter.setCancellable(new Cancellable() { - @Override - public void cancel() throws Exception { - dataSubscription.cancel(); + if (!emitter.isCancelled()) { + emitter.onComplete(); } }); + emitter.setCancellable(dataSubscription::cancel); } /** @@ -89,24 +68,13 @@ public void cancel() throws Exception { * (see {@link Query#subscribe()} for details). */ public static Observable> observable(final Query query) { - return Observable.create(new ObservableOnSubscribe>() { - @Override - public void subscribe(final ObservableEmitter> emitter) throws Exception { - final DataSubscription dataSubscription = query.subscribe().observer(new DataObserver>() { - @Override - public void onData(List data) { - if (!emitter.isDisposed()) { - emitter.onNext(data); - } - } - }); - emitter.setCancellable(new Cancellable() { - @Override - public void cancel() throws Exception { - dataSubscription.cancel(); - } - }); - } + return Observable.create(emitter -> { + final DataSubscription dataSubscription = query.subscribe().observer(data -> { + if (!emitter.isDisposed()) { + emitter.onNext(data); + } + }); + emitter.setCancellable(dataSubscription::cancel); }); } @@ -114,19 +82,13 @@ public void cancel() throws Exception { * The returned Single emits one Query result as a List. */ public static Single> single(final Query query) { - return Single.create(new SingleOnSubscribe>() { - @Override - public void subscribe(final SingleEmitter> emitter) throws Exception { - query.subscribe().single().observer(new DataObserver>() { - @Override - public void onData(List data) { - if (!emitter.isDisposed()) { - emitter.onSuccess(data); - } - } - }); - // no need to cancel, single never subscribes - } + return Single.create(emitter -> { + query.subscribe().single().observer(data -> { + if (!emitter.isDisposed()) { + emitter.onSuccess(data); + } + }); + // no need to cancel, single never subscribes }); } } diff --git a/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java b/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java index 7389effd..a28814a8 100644 --- a/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java +++ b/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java @@ -31,14 +31,15 @@ import io.objectbox.query.FakeQueryPublisher; import io.objectbox.query.MockQuery; -import io.reactivex.Flowable; -import io.reactivex.Observable; -import io.reactivex.Observer; -import io.reactivex.Single; -import io.reactivex.SingleObserver; -import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.core.SingleObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; + import static org.junit.Assert.*; From 3003f0c8bf6c54adbe9234ab6f41a20b21cadd3a Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Mon, 23 Mar 2020 15:45:11 +0100 Subject: [PATCH 27/34] Clean up and update RxJava 3 tests, match internal integration tests. --- .../objectbox/query/FakeQueryPublisher.java | 10 +- .../java/io/objectbox/query/MockQuery.java | 29 ++- .../io/objectbox/rx/QueryObserverTest.java | 218 +++++++++++------- 3 files changed, 153 insertions(+), 104 deletions(-) diff --git a/objectbox-rxjava3/src/test/java/io/objectbox/query/FakeQueryPublisher.java b/objectbox-rxjava3/src/test/java/io/objectbox/query/FakeQueryPublisher.java index 6237c75a..a550b4a1 100644 --- a/objectbox-rxjava3/src/test/java/io/objectbox/query/FakeQueryPublisher.java +++ b/objectbox-rxjava3/src/test/java/io/objectbox/query/FakeQueryPublisher.java @@ -24,9 +24,11 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import javax.annotation.Nullable; + public class FakeQueryPublisher implements DataPublisher> { - private final Set>> observers = new CopyOnWriteArraySet(); + private final Set>> observers = new CopyOnWriteArraySet<>(); private List queryResult = Collections.emptyList(); @@ -39,12 +41,12 @@ public void setQueryResult(List queryResult) { } @Override - public synchronized void subscribe(DataObserver> observer, Object param) { + public synchronized void subscribe(DataObserver> observer, @Nullable Object param) { observers.add(observer); } @Override - public void publishSingle(final DataObserver> observer, Object param) { + public void publishSingle(final DataObserver> observer, @Nullable Object param) { observer.onData(queryResult); } @@ -55,7 +57,7 @@ public void publish() { } @Override - public synchronized void unsubscribe(DataObserver> observer, Object param) { + public synchronized void unsubscribe(DataObserver> observer, @Nullable Object param) { DataPublisherUtils.removeObserverFromCopyOnWriteSet(observers, observer); } diff --git a/objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java b/objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java index 3e86e7c7..8b28d0ab 100644 --- a/objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java +++ b/objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java @@ -16,30 +16,37 @@ package io.objectbox.query; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import java.util.List; import io.objectbox.Box; import io.objectbox.BoxStore; import io.objectbox.reactive.SubscriptionBuilder; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class MockQuery { - private Box box; + private Box box; private BoxStore boxStore; - private final Query query; - private final FakeQueryPublisher fakeQueryPublisher; + private final Query query; + private final FakeQueryPublisher fakeQueryPublisher; public MockQuery(boolean hasOrder) { // box = mock(Box.class); // boxStore = mock(BoxStore.class); // when(box.getStore()).thenReturn(boxStore); - query = mock(Query.class); - fakeQueryPublisher = new FakeQueryPublisher(); - SubscriptionBuilder subscriptionBuilder = new SubscriptionBuilder(fakeQueryPublisher, null, null); + + //noinspection unchecked It's a unit test, casting is fine. + query = (Query) mock(Query.class); + fakeQueryPublisher = new FakeQueryPublisher<>(); + //noinspection ConstantConditions ExecutorService only used for transforms. + SubscriptionBuilder> subscriptionBuilder = new SubscriptionBuilder<>( + fakeQueryPublisher, null, null); when(query.subscribe()).thenReturn(subscriptionBuilder); } - public Box getBox() { + public Box getBox() { return box; } @@ -47,11 +54,11 @@ public BoxStore getBoxStore() { return boxStore; } - public Query getQuery() { + public Query getQuery() { return query; } - public FakeQueryPublisher getFakeQueryPublisher() { + public FakeQueryPublisher getFakeQueryPublisher() { return fakeQueryPublisher; } } diff --git a/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java b/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java index a28814a8..79c0bf22 100644 --- a/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java +++ b/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java @@ -22,6 +22,7 @@ import org.mockito.junit.MockitoJUnitRunner; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -31,7 +32,6 @@ import io.objectbox.query.FakeQueryPublisher; import io.objectbox.query.MockQuery; -import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observer; @@ -41,20 +41,19 @@ import io.reactivex.rxjava3.functions.Consumer; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +/** + * This test has a counterpart in internal integration tests using a real Query and BoxStore. + */ @RunWith(MockitoJUnitRunner.class) -public class QueryObserverTest implements Observer>, SingleObserver>, Consumer { - - private List> receivedChanges = new CopyOnWriteArrayList<>(); - private CountDownLatch latch = new CountDownLatch(1); +public class QueryObserverTest { private MockQuery mockQuery = new MockQuery<>(false); private FakeQueryPublisher publisher = mockQuery.getFakeQueryPublisher(); private List listResult = new ArrayList<>(); - private Throwable error; - - private AtomicInteger completedCount = new AtomicInteger(); @Before public void prep() { @@ -63,114 +62,155 @@ public void prep() { } @Test - public void testObservable() { - Observable observable = RxQuery.observable(mockQuery.getQuery()); - observable.subscribe((Observer) this); - assertLatchCountedDown(latch, 2); - assertEquals(1, receivedChanges.size()); - assertEquals(0, receivedChanges.get(0).size()); - assertNull(error); - - latch = new CountDownLatch(1); - receivedChanges.clear(); + public void observable() { + Observable> observable = RxQuery.observable(mockQuery.getQuery()); + + // Subscribe should emit. + TestObserver testObserver = new TestObserver(); + observable.subscribe(testObserver); + + testObserver.assertLatchCountedDown(2); + assertEquals(1, testObserver.receivedChanges.size()); + assertEquals(0, testObserver.receivedChanges.get(0).size()); + assertNull(testObserver.error); + + // Publish should emit. + testObserver.resetLatch(1); + testObserver.receivedChanges.clear(); + publisher.setQueryResult(listResult); publisher.publish(); - assertLatchCountedDown(latch, 5); - assertEquals(1, receivedChanges.size()); - assertEquals(2, receivedChanges.get(0).size()); + testObserver.assertLatchCountedDown(5); + assertEquals(1, testObserver.receivedChanges.size()); + assertEquals(2, testObserver.receivedChanges.get(0).size()); - assertEquals(0, completedCount.get()); - - //Unsubscribe? - // receivedChanges.clear(); - // latch = new CountDownLatch(1); - // assertLatchCountedDown(latch, 5); - // - // assertEquals(1, receivedChanges.size()); - // assertEquals(3, receivedChanges.get(0).size()); + // Finally, should not be completed. + assertEquals(0, testObserver.completedCount.get()); } @Test - public void testFlowableOneByOne() { + public void flowableOneByOne() { publisher.setQueryResult(listResult); - latch = new CountDownLatch(2); - Flowable flowable = RxQuery.flowableOneByOne(mockQuery.getQuery()); - flowable.subscribe(this); - assertLatchCountedDown(latch, 2); - assertEquals(2, receivedChanges.size()); - assertEquals(1, receivedChanges.get(0).size()); - assertEquals(1, receivedChanges.get(1).size()); - assertNull(error); + Flowable flowable = RxQuery.flowableOneByOne(mockQuery.getQuery()); + + TestObserver testObserver = new TestObserver(); + testObserver.resetLatch(2); + //noinspection ResultOfMethodCallIgnored + flowable.subscribe(testObserver); + + testObserver.assertLatchCountedDown(2); + assertEquals(2, testObserver.receivedChanges.size()); + assertEquals(1, testObserver.receivedChanges.get(0).size()); + assertEquals(1, testObserver.receivedChanges.get(1).size()); + assertNull(testObserver.error); + + testObserver.receivedChanges.clear(); - receivedChanges.clear(); publisher.publish(); - assertNoMoreResults(); + testObserver.assertNoMoreResults(); } @Test - public void testSingle() { + public void single() { publisher.setQueryResult(listResult); - Single single = RxQuery.single(mockQuery.getQuery()); - single.subscribe((SingleObserver) this); - assertLatchCountedDown(latch, 2); - assertEquals(1, receivedChanges.size()); - assertEquals(2, receivedChanges.get(0).size()); - receivedChanges.clear(); + Single> single = RxQuery.single(mockQuery.getQuery()); + + TestObserver testObserver = new TestObserver(); + single.subscribe(testObserver); + + testObserver.assertLatchCountedDown(2); + assertEquals(1, testObserver.receivedChanges.size()); + assertEquals(2, testObserver.receivedChanges.get(0).size()); + + testObserver.receivedChanges.clear(); + publisher.publish(); - assertNoMoreResults(); + testObserver.assertNoMoreResults(); } - protected void assertNoMoreResults() { - assertEquals(0, receivedChanges.size()); - try { - Thread.sleep(20); - } catch (InterruptedException e) { - throw new RuntimeException(e); + private static class TestObserver implements Observer>, SingleObserver>, Consumer { + + List> receivedChanges = new CopyOnWriteArrayList<>(); + CountDownLatch latch = new CountDownLatch(1); + Throwable error; + AtomicInteger completedCount = new AtomicInteger(); + + private void log(String message) { + System.out.println("TestObserver: " + message); } - assertEquals(0, receivedChanges.size()); - } - protected void assertLatchCountedDown(CountDownLatch latch, int seconds) { - try { - assertTrue(latch.await(seconds, TimeUnit.SECONDS)); - } catch (InterruptedException e) { - throw new RuntimeException(e); + void printEvents() { + int count = receivedChanges.size(); + log("Received " + count + " event(s):"); + for (int i = 0; i < count; i++) { + List receivedChange = receivedChanges.get(i); + log((i + 1) + "/" + count + ": size=" + receivedChange.size() + + "; items=" + Arrays.toString(receivedChange.toArray())); + } } - } - @Override - public void onSubscribe(Disposable d) { + void resetLatch(int count) { + latch = new CountDownLatch(count); + } - } + void assertLatchCountedDown(int seconds) { + try { + assertTrue(latch.await(seconds, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + printEvents(); + } - @Override - public void onSuccess(List queryResult) { - receivedChanges.add(queryResult); - latch.countDown(); - } + void assertNoMoreResults() { + assertEquals(0, receivedChanges.size()); + try { + Thread.sleep(20); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + assertEquals(0, receivedChanges.size()); + } - @Override - public void onNext(List queryResult) { - receivedChanges.add(queryResult); - latch.countDown(); - } + @Override + public void onSubscribe(Disposable d) { + log("onSubscribe"); + } - @Override - public void onError(Throwable e) { - error = e; - } + @Override + public void onNext(List t) { + log("onNext"); + receivedChanges.add(t); + latch.countDown(); + } - @Override - public void onComplete() { - completedCount.incrementAndGet(); - } + @Override + public void onError(Throwable e) { + log("onError"); + error = e; + } + + @Override + public void onComplete() { + log("onComplete"); + completedCount.incrementAndGet(); + } + + @Override + public void accept(String t) { + log("accept"); + receivedChanges.add(Collections.singletonList(t)); + latch.countDown(); + } - @Override - public void accept(@NonNull String s) throws Exception { - receivedChanges.add(Collections.singletonList(s)); - latch.countDown(); + @Override + public void onSuccess(List t) { + log("onSuccess"); + receivedChanges.add(t); + latch.countDown(); + } } } From bce279b7d1722554219e00c3e9b406ffe7b247d4 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 24 Mar 2020 07:32:59 +0100 Subject: [PATCH 28/34] RxBoxStore: remove unused code, suppress expected raw type warning. --- .../src/main/java/io/objectbox/rx/RxBoxStore.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java b/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java index c1117abc..054719c3 100644 --- a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java +++ b/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java @@ -28,7 +28,8 @@ public abstract class RxBoxStore { * Using the returned Observable, you can be notified about data changes. * Once a transaction is committed, you will get info on classes with changed Objects. */ - public static Observable observable(final BoxStore boxStore) { + @SuppressWarnings("rawtypes") // BoxStore observer may return any (entity) type. + public static Observable observable(BoxStore boxStore) { return Observable.create(emitter -> { final DataSubscription dataSubscription = boxStore.subscribe().observer(data -> { if (!emitter.isDisposed()) { From d008764aa773c2aa952ac04a6153be5c82d1fb0d Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 24 Mar 2020 07:38:23 +0100 Subject: [PATCH 29/34] Move to rx3 package to allow side-by-side usage with RxJava 2. --- .../src/main/java/io/objectbox/{rx => rx3}/RxBoxStore.java | 2 +- .../src/main/java/io/objectbox/{rx => rx3}/RxQuery.java | 2 +- .../test/java/io/objectbox/{rx => rx3}/QueryObserverTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename objectbox-rxjava3/src/main/java/io/objectbox/{rx => rx3}/RxBoxStore.java (98%) rename objectbox-rxjava3/src/main/java/io/objectbox/{rx => rx3}/RxQuery.java (99%) rename objectbox-rxjava3/src/test/java/io/objectbox/{rx => rx3}/QueryObserverTest.java (99%) diff --git a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java b/objectbox-rxjava3/src/main/java/io/objectbox/rx3/RxBoxStore.java similarity index 98% rename from objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java rename to objectbox-rxjava3/src/main/java/io/objectbox/rx3/RxBoxStore.java index 054719c3..79f8f1d0 100644 --- a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxBoxStore.java +++ b/objectbox-rxjava3/src/main/java/io/objectbox/rx3/RxBoxStore.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.objectbox.rx; +package io.objectbox.rx3; import io.objectbox.BoxStore; import io.objectbox.reactive.DataSubscription; diff --git a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxQuery.java b/objectbox-rxjava3/src/main/java/io/objectbox/rx3/RxQuery.java similarity index 99% rename from objectbox-rxjava3/src/main/java/io/objectbox/rx/RxQuery.java rename to objectbox-rxjava3/src/main/java/io/objectbox/rx3/RxQuery.java index ad278d31..feadfdd1 100644 --- a/objectbox-rxjava3/src/main/java/io/objectbox/rx/RxQuery.java +++ b/objectbox-rxjava3/src/main/java/io/objectbox/rx3/RxQuery.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.objectbox.rx; +package io.objectbox.rx3; import java.util.List; diff --git a/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java b/objectbox-rxjava3/src/test/java/io/objectbox/rx3/QueryObserverTest.java similarity index 99% rename from objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java rename to objectbox-rxjava3/src/test/java/io/objectbox/rx3/QueryObserverTest.java index 79c0bf22..bdf70d98 100644 --- a/objectbox-rxjava3/src/test/java/io/objectbox/rx/QueryObserverTest.java +++ b/objectbox-rxjava3/src/test/java/io/objectbox/rx3/QueryObserverTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.objectbox.rx; +package io.objectbox.rx3; import org.junit.Before; import org.junit.Test; From 8093b805b7dde271a0e7457274cb3af4d7d719d8 Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 24 Mar 2020 07:38:39 +0100 Subject: [PATCH 30/34] Jenkinsfile: also run rx3.QueryObserverTest. --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index 6678b869..33807a1d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -54,6 +54,7 @@ pipeline { "--tests io.objectbox.FunctionalTestSuite " + "--tests io.objectbox.test.proguard.ObfuscatedEntityTest " + "--tests io.objectbox.rx.QueryObserverTest " + + "--tests io.objectbox.rx3.QueryObserverTest " + "assemble" } } From 2fdf973c6885b7b2c9bb3da59735d61d0e69e2ce Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 24 Mar 2020 08:01:54 +0100 Subject: [PATCH 31/34] Add migration notes to READMEs. - Fix examples. - Do not link to unrelated example, it's confusing. --- objectbox-rxjava/README.md | 7 ++++--- objectbox-rxjava3/README.md | 14 +++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/objectbox-rxjava/README.md b/objectbox-rxjava/README.md index 5bca7836..d349f2fc 100644 --- a/objectbox-rxjava/README.md +++ b/objectbox-rxjava/README.md @@ -1,3 +1,6 @@ +:information_source: This library will receive no new features. +Development will continue with the [RxJava 3 APIs for ObjectBox](/objectbox-rxjava3). + RxJava 2 APIs for ObjectBox =========================== While ObjectBox has [data observers and reactive extensions](https://docs.objectbox.io/data-observers-and-rx) built-in, @@ -13,7 +16,7 @@ For general object changes, you can use `RxBoxStore` to create an `Observable`. For example to get query results and subscribe to future updates (Object changes will automatically emmit new data): ```java -Query query = box.query().build(); +Query query = box.query().build(); RxQuery.observable(query).subscribe(this); ``` @@ -27,5 +30,3 @@ implementation "io.objectbox:objectbox-rxjava:$objectboxVersion" Links ----- [Data Observers and Rx Documentation](https://docs.objectbox.io/data-observers-and-rx) - -[Note App example](https://github.com/objectbox/objectbox-examples/blob/master/objectbox-example/src/main/java/io/objectbox/example/ReactiveNoteActivity.java) diff --git a/objectbox-rxjava3/README.md b/objectbox-rxjava3/README.md index bfc79769..91d84eab 100644 --- a/objectbox-rxjava3/README.md +++ b/objectbox-rxjava3/README.md @@ -13,7 +13,7 @@ For general object changes, you can use `RxBoxStore` to create an `Observable`. For example to get query results and subscribe to future updates (Object changes will automatically emmit new data): ```java -Query query = box.query().build(); +Query query = box.query().build(); RxQuery.observable(query).subscribe(this); ``` @@ -24,8 +24,16 @@ Grab via Gradle: implementation "io.objectbox:objectbox-rxjava3:$objectboxVersion" ``` +Migrating from RxJava 2 +----------------------- + +If you have previously used the ObjectBox RxJava library note the following changes: + +- The location of the dependency has changed to `objectbox-rxjava3` (see above). +- The package name has changed to `io.objectbox.rx3` (from `io.objectbox.rx`). + +This should allow using both versions side-by-side while you migrate your code to RxJava 3. + Links ----- [Data Observers and Rx Documentation](https://docs.objectbox.io/data-observers-and-rx) - -[Note App example](https://github.com/objectbox/objectbox-examples/blob/master/objectbox-example/src/main/java/io/objectbox/example/ReactiveNoteActivity.java) From a0a45022bdceeea243179e848d2a20a71c19bf1d Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 24 Mar 2020 10:22:40 +0100 Subject: [PATCH 32/34] Add Kotlin extensions to build Rx types, requires Java 8 for Kotlin library. --- objectbox-kotlin/build.gradle | 3 +- .../main/kotlin/io/objectbox/kotlin/Query.kt | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Query.kt diff --git a/objectbox-kotlin/build.gradle b/objectbox-kotlin/build.gradle index e29c8ffc..966f565c 100644 --- a/objectbox-kotlin/build.gradle +++ b/objectbox-kotlin/build.gradle @@ -20,7 +20,7 @@ buildscript { apply plugin: 'kotlin' apply plugin: 'org.jetbrains.dokka' -sourceCompatibility = 1.7 +sourceCompatibility = 1.8 dokka { outputFormat = 'html' @@ -58,6 +58,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile project(':objectbox-java') + compileOnly project(':objectbox-rxjava3') } diff --git a/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Query.kt b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Query.kt new file mode 100644 index 00000000..e268b7bc --- /dev/null +++ b/objectbox-kotlin/src/main/kotlin/io/objectbox/kotlin/Query.kt @@ -0,0 +1,29 @@ +package io.objectbox.kotlin + +import io.objectbox.query.Query +import io.objectbox.rx3.RxQuery +import io.reactivex.rxjava3.core.BackpressureStrategy +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single + +/** + * Shortcut for [`RxQuery.flowableOneByOne(query, strategy)`][RxQuery.flowableOneByOne]. + */ +fun Query.flowableOneByOne(strategy: BackpressureStrategy = BackpressureStrategy.BUFFER): Flowable { + return RxQuery.flowableOneByOne(this, strategy) +} + +/** + * Shortcut for [`RxQuery.observable(query)`][RxQuery.observable]. + */ +fun Query.observable(): Observable> { + return RxQuery.observable(this) +} + +/** + * Shortcut for [`RxQuery.single(query)`][RxQuery.single]. + */ +fun Query.single(): Single> { + return RxQuery.single(this) +} From cf8ed4e07daa53f40e915bd04ac82aab399fd4ff Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 24 Mar 2020 08:39:03 +0100 Subject: [PATCH 33/34] MockQuery: update signature of SubscriptionBuilder constructor. --- .../src/test/java/io/objectbox/query/MockQuery.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java b/objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java index 8b28d0ab..30c44254 100644 --- a/objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java +++ b/objectbox-rxjava3/src/test/java/io/objectbox/query/MockQuery.java @@ -40,9 +40,8 @@ public MockQuery(boolean hasOrder) { //noinspection unchecked It's a unit test, casting is fine. query = (Query) mock(Query.class); fakeQueryPublisher = new FakeQueryPublisher<>(); - //noinspection ConstantConditions ExecutorService only used for transforms. SubscriptionBuilder> subscriptionBuilder = new SubscriptionBuilder<>( - fakeQueryPublisher, null, null); + fakeQueryPublisher, null); when(query.subscribe()).thenReturn(subscriptionBuilder); } From 213b99703a81e4ff941fde476d91d791d20421eb Mon Sep 17 00:00:00 2001 From: greenrobot Team Date: Tue, 24 Mar 2020 12:57:42 +0100 Subject: [PATCH 34/34] Prepare version 3.0.0-alpha2. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4f3b2ede..103b766f 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { ext { // Typically, only edit those two: def objectboxVersionNumber = '3.0.0-alpha2' // without "-SNAPSHOT", e.g. '2.5.0' or '2.4.0-RC' - def objectboxVersionRelease = false // set to true for releasing to ignore versionPostFix to avoid e.g. "-dev" versions + def objectboxVersionRelease = true // set to true for releasing to ignore versionPostFix to avoid e.g. "-dev" versions // version post fix: '-' or '' if not defined; e.g. used by CI to pass in branch name // def versionPostFixValue = project.findProperty('versionPostFix')