/* * Copyright 2017 Realm Inc. * * 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.realm.internal; import java.util.ConcurrentModificationException; import java.util.Date; import java.util.NoSuchElementException; import io.realm.OrderedCollectionChangeSet; import io.realm.OrderedRealmCollectionChangeListener; import io.realm.RealmChangeListener; /** * Java wrapper of Object Store Results class. * It is the backend of binding's query results, link lists and back links. */ @Keep public class Collection implements NativeObject { private static final String CLOSED_REALM_MESSAGE = "This Realm instance has already been closed, making it unusable."; private static class CollectionObserverPair<T> extends ObserverPairList.ObserverPair<T, Object> { public CollectionObserverPair(T observer, Object listener) { super(observer, listener); } public void onChange(T observer, OrderedCollectionChangeSet changes) { if (listener instanceof OrderedRealmCollectionChangeListener) { //noinspection unchecked ((OrderedRealmCollectionChangeListener<T>) listener).onChange(observer, changes); } else if (listener instanceof RealmChangeListener) { //noinspection unchecked ((RealmChangeListener<T>) listener).onChange(observer); } else { throw new RuntimeException("Unsupported listener type: " + listener); } } } private static class RealmChangeListenerWrapper<T> implements OrderedRealmCollectionChangeListener<T> { private final RealmChangeListener<T> listener; RealmChangeListenerWrapper(RealmChangeListener<T> listener) { this.listener = listener; } @Override public void onChange(T collection, OrderedCollectionChangeSet changes) { listener.onChange(collection); } @Override public boolean equals(Object obj) { return obj instanceof RealmChangeListenerWrapper && listener == ((RealmChangeListenerWrapper) obj).listener; } @Override public int hashCode() { return listener.hashCode(); } } private static class Callback implements ObserverPairList.Callback<CollectionObserverPair> { private final OrderedCollectionChangeSet changeSet; Callback(OrderedCollectionChangeSet changeSet) { this.changeSet = changeSet; } @Override public void onCalled(CollectionObserverPair pair, Object observer) { //noinspection unchecked pair.onChange(observer, changeSet); } } // Custom Collection iterator. It ensures that we only iterate on a Realm collection that hasn't changed. public static abstract class Iterator<T> implements java.util.Iterator<T> { Collection iteratorCollection; protected int pos = -1; public Iterator(Collection collection) { if (collection.sharedRealm.isClosed()) { throw new IllegalStateException(CLOSED_REALM_MESSAGE); } this.iteratorCollection = collection; if (collection.isSnapshot) { // No need to detach a snapshot. return; } if (collection.sharedRealm.isInTransaction()) { detach(); } else { iteratorCollection.sharedRealm.addIterator(this); } } /** * {@inheritDoc} */ @Override public boolean hasNext() { checkValid(); return pos + 1 < iteratorCollection.size(); } /** * {@inheritDoc} */ @Override public T next() { checkValid(); pos++; if (pos >= iteratorCollection.size()) { throw new NoSuchElementException("Cannot access index " + pos + " when size is " + iteratorCollection.size() + ". Remember to check hasNext() before using next()."); } return get(pos); } /** * Not supported by Realm collection iterators. * * @throws UnsupportedOperationException */ @Override @Deprecated public void remove() { throw new UnsupportedOperationException("remove() is not supported by RealmResults iterators."); } void detach() { iteratorCollection = iteratorCollection.createSnapshot(); } // The iterator becomes invalid after receiving a remote change notification. In Java, the destruction of // iterator totally depends on GC. If we just detach those iterators when remote change notification received // like what realm-cocoa does, we will have a massive overhead since all the iterators created in the previous // event loop need to be detached. void invalidate() { iteratorCollection = null; } void checkValid() { if (iteratorCollection == null) { throw new ConcurrentModificationException( "No outside changes to a Realm is allowed while iterating a living Realm collection."); } } T get(int pos) { return convertRowToObject(iteratorCollection.getUncheckedRow(pos)); } // Returns the RealmModel by given row in this list. This has to be implemented in the upper layer since // we don't have information about the object types in the internal package. protected abstract T convertRowToObject(UncheckedRow row); } // Custom Realm collection list iterator. public static abstract class ListIterator<T> extends Iterator<T> implements java.util.ListIterator<T> { public ListIterator(Collection collection, int start) { super(collection); if (start >= 0 && start <= iteratorCollection.size()) { pos = start - 1; } else { throw new IndexOutOfBoundsException("Starting location must be a valid index: [0, " + (iteratorCollection.size() - 1) + "]. Yours was " + start); } } /** * Unsupported by Realm collection iterators. * * @throws UnsupportedOperationException */ @Override @Deprecated public void add(T object) { throw new UnsupportedOperationException("Adding an element is not supported. Use Realm.createObject() instead."); } /** * {@inheritDoc} */ @Override public boolean hasPrevious() { checkValid(); return pos >= 0; } /** * {@inheritDoc} */ @Override public int nextIndex() { checkValid(); return pos + 1; } /** * {@inheritDoc} */ @Override public T previous() { checkValid(); try { T obj = get(pos); pos--; return obj; } catch (IndexOutOfBoundsException e) { throw new NoSuchElementException("Cannot access index less than zero. This was " + pos + ". Remember to check hasPrevious() before using previous()."); } } /** * {@inheritDoc} */ @Override public int previousIndex() { checkValid(); return pos; } /** * Unsupported by RealmResults iterators. * * @throws UnsupportedOperationException */ @Override @Deprecated public void set(T object) { throw new UnsupportedOperationException("Replacing and element is not supported."); } } private final long nativePtr; private static final long nativeFinalizerPtr = nativeGetFinalizerPtr(); private final SharedRealm sharedRealm; private final NativeContext context; private final Table table; private boolean loaded; private boolean isSnapshot = false; private final ObserverPairList<CollectionObserverPair> observerPairs = new ObserverPairList<CollectionObserverPair>(); // Public for static checking in JNI @SuppressWarnings("WeakerAccess") public static final byte AGGREGATE_FUNCTION_MINIMUM = 1; @SuppressWarnings("WeakerAccess") public static final byte AGGREGATE_FUNCTION_MAXIMUM = 2; @SuppressWarnings("WeakerAccess") public static final byte AGGREGATE_FUNCTION_AVERAGE = 3; @SuppressWarnings("WeakerAccess") public static final byte AGGREGATE_FUNCTION_SUM = 4; public enum Aggregate { MINIMUM(AGGREGATE_FUNCTION_MINIMUM), MAXIMUM(AGGREGATE_FUNCTION_MAXIMUM), AVERAGE(AGGREGATE_FUNCTION_AVERAGE), SUM(AGGREGATE_FUNCTION_SUM); private final byte value; Aggregate(byte value) { this.value = value; } public byte getValue() { return value; } } @SuppressWarnings("WeakerAccess") public static final byte MODE_EMPTY = 0; @SuppressWarnings("WeakerAccess") public static final byte MODE_TABLE = 1; @SuppressWarnings("WeakerAccess") public static final byte MODE_QUERY = 2; @SuppressWarnings("WeakerAccess") public static final byte MODE_LINKVIEW = 3; @SuppressWarnings("WeakerAccess") public static final byte MODE_TABLEVIEW = 4; public enum Mode { EMPTY, // Backed by nothing (for missing tables) TABLE, // Backed directly by a Table QUERY, // Backed by a query that has not yet been turned into a TableView LINKVIEW, // Backed directly by a LinkView TABLEVIEW; // Backed by a TableView created from a Query static Mode getByValue(byte value) { switch (value) { case MODE_EMPTY: return EMPTY; case MODE_TABLE: return TABLE; case MODE_QUERY: return QUERY; case MODE_LINKVIEW: return LINKVIEW; case MODE_TABLEVIEW: return TABLEVIEW; default: throw new IllegalArgumentException("Invalid value: " + value); } } } public static Collection createBacklinksCollection(SharedRealm realm, UncheckedRow row, Table srcTable, String srcFieldName) { long backlinksPtr = nativeCreateResultsFromBacklinks( realm.getNativePtr(), row.getNativePtr(), srcTable.getNativePtr(), srcTable.getColumnIndex(srcFieldName)); return new Collection(realm, srcTable, backlinksPtr, true); } public Collection(SharedRealm sharedRealm, TableQuery query, SortDescriptor sortDescriptor, SortDescriptor distinctDescriptor) { query.validateQuery(); this.nativePtr = nativeCreateResults(sharedRealm.getNativePtr(), query.getNativePtr(), sortDescriptor, distinctDescriptor); this.sharedRealm = sharedRealm; this.context = sharedRealm.context; this.table = query.getTable(); this.context.addReference(this); this.loaded = false; } public Collection(SharedRealm sharedRealm, TableQuery query, SortDescriptor sortDescriptor) { this(sharedRealm, query, sortDescriptor, null); } public Collection(SharedRealm sharedRealm, TableQuery query) { this(sharedRealm, query, null, null); } public Collection(SharedRealm sharedRealm, LinkView linkView, SortDescriptor sortDescriptor) { this.nativePtr = nativeCreateResultsFromLinkView(sharedRealm.getNativePtr(), linkView.getNativePtr(), sortDescriptor); this.sharedRealm = sharedRealm; this.context = sharedRealm.context; this.table = linkView.getTargetTable(); this.context.addReference(this); // Collection created from LinkView is loaded by default. So that the listener will be triggered first time // with empty change set. this.loaded = true; } private Collection(SharedRealm sharedRealm, Table table, long nativePtr) { this(sharedRealm, table, nativePtr, false); } private Collection(SharedRealm sharedRealm, Table table, long nativePtr, boolean loaded) { this.sharedRealm = sharedRealm; this.context = sharedRealm.context; this.table = table; this.nativePtr = nativePtr; this.context.addReference(this); this.loaded = loaded; } public Collection createSnapshot() { if (isSnapshot) { return this; } Collection collection = new Collection(sharedRealm, table, nativeCreateSnapshot(nativePtr)); collection.isSnapshot = true; return collection; } @Override public long getNativePtr() { return nativePtr; } @Override public long getNativeFinalizerPtr() { return nativeFinalizerPtr; } public UncheckedRow getUncheckedRow(int index) { return table.getUncheckedRowByPointer(nativeGetRow(nativePtr, index)); } public UncheckedRow firstUncheckedRow() { long rowPtr = nativeFirstRow(nativePtr); if (rowPtr != 0) { return table.getUncheckedRowByPointer(rowPtr); } return null; } public UncheckedRow lastUncheckedRow() { long rowPtr = nativeLastRow(nativePtr); if (rowPtr != 0) { return table.getUncheckedRowByPointer(rowPtr); } return null; } public Table getTable() { return table; } public TableQuery where() { long nativeQueryPtr = nativeWhere(nativePtr); return new TableQuery(this.context, this.table, nativeQueryPtr); } public Number aggregateNumber(Aggregate aggregateMethod, long columnIndex) { return (Number) nativeAggregate(nativePtr, columnIndex, aggregateMethod.getValue()); } public Date aggregateDate(Aggregate aggregateMethod, long columnIndex) { return (Date) nativeAggregate(nativePtr, columnIndex, aggregateMethod.getValue()); } public long size() { return nativeSize(nativePtr); } public void clear() { nativeClear(nativePtr); } public Collection sort(SortDescriptor sortDescriptor) { return new Collection(sharedRealm, table, nativeSort(nativePtr, sortDescriptor)); } public Collection distinct(SortDescriptor distinctDescriptor) { return new Collection(sharedRealm, table, nativeDistinct(nativePtr, distinctDescriptor)); } public boolean contains(UncheckedRow row) { return nativeContains(nativePtr, row.getNativePtr()); } public int indexOf(UncheckedRow row) { long index = nativeIndexOf(nativePtr, row.getNativePtr()); return (index > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) index; } public int indexOf(long sourceRowIndex) { long index = nativeIndexOfBySourceRowIndex(nativePtr, sourceRowIndex); return (index > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) index; } public void delete(long index) { nativeDelete(nativePtr, index); } public boolean deleteFirst() { return nativeDeleteFirst(nativePtr); } public boolean deleteLast() { return nativeDeleteLast(nativePtr); } public <T> void addListener(T observer, OrderedRealmCollectionChangeListener<T> listener) { if (observerPairs.isEmpty()) { nativeStartListening(nativePtr); } CollectionObserverPair<T> collectionObserverPair = new CollectionObserverPair<T>(observer, listener); observerPairs.add(collectionObserverPair); } public <T> void addListener(T observer, RealmChangeListener<T> listener) { addListener(observer, new RealmChangeListenerWrapper<T>(listener)); } public <T> void removeListener(T observer, OrderedRealmCollectionChangeListener<T> listener) { observerPairs.remove(observer, listener); if (observerPairs.isEmpty()) { nativeStopListening(nativePtr); } } public <T> void removeListener(T observer, RealmChangeListener<T> listener) { removeListener(observer, new RealmChangeListenerWrapper<T>(listener)); } public void removeAllListeners() { observerPairs.clear(); nativeStopListening(nativePtr); } public boolean isValid() { return nativeIsValid(nativePtr); } // Called by JNI @SuppressWarnings("unused") private void notifyChangeListeners(long nativeChangeSetPtr) { if (nativeChangeSetPtr == 0 && isLoaded()) { return; } boolean wasLoaded = loaded; loaded = true; // Object Store compute the change set between the SharedGroup versions when the query created and the latest. // So it is possible it deliver a non-empty change set for the first async query returns. In this case, we // return an empty change set to user since it is considered as the first time async query returns. observerPairs.foreach(new Callback(nativeChangeSetPtr == 0 || !wasLoaded ? null : new CollectionChangeSet(nativeChangeSetPtr))); } public Mode getMode() { return Mode.getByValue(nativeGetMode(nativePtr)); } // The Results of Object Store will be queried asynchronously in nature. But we do have to support "sync" query by // Java like RealmQuery.findAll(). // The flag is used for following cases: // 1. For sync query, loaded will be set to true when collection is created. So we will bypass the first trigger of // listener if it comes with empty change set from Object Store since we assume user already got the query // result. // 2. For async query, when load() gets called with loaded not set, the listener should be triggered with empty // change set since it is considered as query first returned. // 3. If the listener triggered with empty change set after load() called for async queries, it is treated as the // same case as 1). // TODO: Results built from a LinkView has not been considered yet. Maybe it should bet set as loaded when create. public boolean isLoaded() { return loaded; } public void load() { if (loaded) { return; } notifyChangeListeners(0); } private static native long nativeGetFinalizerPtr(); private static native long nativeCreateResults(long sharedRealmNativePtr, long queryNativePtr, SortDescriptor sortDesc, SortDescriptor distinctDesc); private static native long nativeCreateResultsFromLinkView(long sharedRealmNativePtr, long linkViewPtr, SortDescriptor sortDesc); private static native long nativeCreateSnapshot(long nativePtr); private static native long nativeGetRow(long nativePtr, int index); private static native long nativeFirstRow(long nativePtr); private static native long nativeLastRow(long nativePtr); private static native boolean nativeContains(long nativePtr, long nativeRowPtr); private static native void nativeClear(long nativePtr); private static native long nativeSize(long nativePtr); private static native Object nativeAggregate(long nativePtr, long columnIndex, byte aggregateFunc); private static native long nativeSort(long nativePtr, SortDescriptor sortDesc); private static native long nativeDistinct(long nativePtr, SortDescriptor distinctDesc); private static native boolean nativeDeleteFirst(long nativePtr); private static native boolean nativeDeleteLast(long nativePtr); private static native void nativeDelete(long nativePtr, long index); // Non-static, we need this Collection object in JNI. private native void nativeStartListening(long nativePtr); private native void nativeStopListening(long nativePtr); private static native long nativeWhere(long nativePtr); private static native long nativeIndexOf(long nativePtr, long rowNativePtr); private static native long nativeIndexOfBySourceRowIndex(long nativePtr, long sourceRowIndex); private static native boolean nativeIsValid(long nativePtr); private static native byte nativeGetMode(long nativePtr); private static native long nativeCreateResultsFromBacklinks(long sharedRealmNativePtr, long rowNativePtr, long srcTableNativePtr, long srColIndex); }