/*
* Copyright 2016 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.io.Closeable;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import io.realm.RealmConfiguration;
import io.realm.internal.android.AndroidCapabilities;
import io.realm.internal.android.AndroidRealmNotifier;
public final class SharedRealm implements Closeable, NativeObject {
// Const value for RealmFileException conversion
public static final byte FILE_EXCEPTION_KIND_ACCESS_ERROR = 0;
public static final byte FILE_EXCEPTION_KIND_BAD_HISTORY = 1;
public static final byte FILE_EXCEPTION_KIND_PERMISSION_DENIED = 2;
public static final byte FILE_EXCEPTION_KIND_EXISTS = 3;
public static final byte FILE_EXCEPTION_KIND_NOT_FOUND = 4;
public static final byte FILE_EXCEPTION_KIND_INCOMPATIBLE_LOCK_FILE = 5;
public static final byte FILE_EXCEPTION_KIND_FORMAT_UPGRADE_REQUIRED = 6;
private static final long nativeFinalizerPtr = nativeGetFinalizerPtr();
public static void initialize(File tempDirectory) {
if (SharedRealm.temporaryDirectory != null) {
// already initialized
return;
}
if (tempDirectory == null) {
throw new IllegalArgumentException("'tempDirectory' must not be null.");
}
String temporaryDirectoryPath = tempDirectory.getAbsolutePath();
if (!tempDirectory.isDirectory() && !tempDirectory.mkdirs() && !tempDirectory.isDirectory()) {
throw new IOException("failed to create temporary directory: " + temporaryDirectoryPath);
}
if (!temporaryDirectoryPath.endsWith("/")) {
temporaryDirectoryPath += "/";
}
nativeInit(temporaryDirectoryPath);
SharedRealm.temporaryDirectory = tempDirectory;
}
public static File getTemporaryDirectory() {
return temporaryDirectory;
}
private static volatile File temporaryDirectory;
public enum Durability {
FULL(0),
MEM_ONLY(1);
final int value;
Durability(int value) {
this.value = value;
}
}
// Public for static checking in JNI
@SuppressWarnings("WeakerAccess")
public static final byte SCHEMA_MODE_VALUE_AUTOMATIC = 0;
@SuppressWarnings("WeakerAccess")
public static final byte SCHEMA_MODE_VALUE_READONLY = 1;
@SuppressWarnings("WeakerAccess")
public static final byte SCHEMA_MODE_VALUE_RESET_FILE = 2;
@SuppressWarnings("WeakerAccess")
public static final byte SCHEMA_MODE_VALUE_ADDITIVE = 3;
@SuppressWarnings("WeakerAccess")
public static final byte SCHEMA_MODE_VALUE_MANUAL = 4;
@SuppressWarnings("WeakerAccess")
public enum SchemaMode {
SCHEMA_MODE_AUTOMATIC(SCHEMA_MODE_VALUE_AUTOMATIC),
SCHEMA_MODE_READONLY(SCHEMA_MODE_VALUE_READONLY),
SCHEMA_MODE_RESET_FILE(SCHEMA_MODE_VALUE_RESET_FILE),
SCHEMA_MODE_ADDITIVE(SCHEMA_MODE_VALUE_ADDITIVE),
SCHEMA_MODE_MANUAL(SCHEMA_MODE_VALUE_MANUAL);
final byte value;
SchemaMode(byte value) {
this.value = value;
}
public byte getNativeValue() {
return value;
}
}
private final List<WeakReference<PendingRow>> pendingRows = new CopyOnWriteArrayList<>();
public final List<WeakReference<Collection>> collections = new CopyOnWriteArrayList<>();
public final List<WeakReference<Collection.Iterator>> iterators = new ArrayList<>();
// JNI will only hold a weak global ref to this.
public final RealmNotifier realmNotifier;
public final Capabilities capabilities;
public static class VersionID implements Comparable<VersionID> {
public final long version;
public final long index;
VersionID(long version, long index) {
this.version = version;
this.index = index;
}
@Override
public int compareTo(@SuppressWarnings("NullableProblems") VersionID another) {
//noinspection ConstantConditions
if (another == null) {
throw new IllegalArgumentException("Version cannot be compared to a null value.");
}
if (version > another.version) {
return 1;
} else if (version < another.version) {
return -1;
} else {
return 0;
}
}
@Override
public String toString() {
return "VersionID{" +
"version=" + version +
", index=" + index +
'}';
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
VersionID versionID = (VersionID) object;
return (version == versionID.version && index == versionID.index);
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (int) (version ^ (version >>> 32));
result = 31 * result + (int) (index ^ (index >>> 32));
return result;
}
}
public interface SchemaVersionListener {
void onSchemaVersionChanged(long currentVersion);
}
private final SchemaVersionListener schemaChangeListener;
private final RealmConfiguration configuration;
private final long nativePtr;
private long lastSchemaVersion;
final NativeContext context;
private SharedRealm(long nativeConfigPtr,
RealmConfiguration configuration,
SchemaVersionListener schemaVersionListener) {
Capabilities capabilities = new AndroidCapabilities();
RealmNotifier realmNotifier = new AndroidRealmNotifier(this, capabilities);
this.nativePtr = nativeGetSharedRealm(nativeConfigPtr, realmNotifier);
this.configuration = configuration;
this.capabilities = capabilities;
this.realmNotifier = realmNotifier;
this.schemaChangeListener = schemaVersionListener;
context = new NativeContext();
context.addReference(this);
this.lastSchemaVersion = schemaVersionListener == null ? -1L : getSchemaVersion();
nativeSetAutoRefresh(nativePtr, capabilities.canDeliverNotification());
}
// This will create a SharedRealm where autoChangeNotifications is false,
// If autoChangeNotifications is true, an additional SharedGroup might be created in the OS's external commit helper.
// That is not needed for some cases: eg.: An extra opened SharedGroup will cause a compact failure.
public static SharedRealm getInstance(RealmConfiguration config) {
return getInstance(config, null, false);
}
public static SharedRealm getInstance(RealmConfiguration config, SchemaVersionListener schemaVersionListener,
boolean autoChangeNotifications) {
String[] syncUserConf = ObjectServerFacade.getSyncFacadeIfPossible().getUserAndServerUrl(config);
String syncUserIdentifier = syncUserConf[0];
String syncRealmUrl = syncUserConf[1];
String syncRealmAuthUrl = syncUserConf[2];
String syncRefreshToken = syncUserConf[3];
final boolean enableCaching = false; // Handled in Java currently
final boolean enableFormatUpgrade = true;
long nativeConfigPtr = nativeCreateConfig(
config.getPath(),
config.getEncryptionKey(),
syncRealmUrl != null ? SchemaMode.SCHEMA_MODE_ADDITIVE.getNativeValue() : SchemaMode.SCHEMA_MODE_MANUAL.getNativeValue(),
config.getDurability() == Durability.MEM_ONLY,
enableCaching,
config.getSchemaVersion(),
enableFormatUpgrade,
autoChangeNotifications,
syncRealmUrl,
syncRealmAuthUrl,
syncUserIdentifier,
syncRefreshToken);
try {
ObjectServerFacade.getSyncFacadeIfPossible().wrapObjectStoreSessionIfRequired(config);
return new SharedRealm(nativeConfigPtr, config, schemaVersionListener);
} finally {
nativeCloseConfig(nativeConfigPtr);
}
}
public void beginTransaction() {
beginTransaction(false);
}
public void beginTransaction(boolean ignoreReadOnly) {
// TODO ReadOnly is also supported by the Object Store Schema, but until we support that we need to enforce it
// ourselves.
if (!ignoreReadOnly && configuration.isReadOnly()) {
throw new IllegalStateException("Write transactions cannot be used when a Realm is marked as read-only.");
}
detachIterators();
executePendingRowQueries();
nativeBeginTransaction(nativePtr);
invokeSchemaChangeListenerIfSchemaChanged();
}
public void commitTransaction() {
nativeCommitTransaction(nativePtr);
}
public void cancelTransaction() {
nativeCancelTransaction(nativePtr);
}
public boolean isInTransaction() {
return nativeIsInTransaction(nativePtr);
}
public void setSchemaVersion(long schemaVersion) {
nativeSetVersion(nativePtr, schemaVersion);
}
public long getSchemaVersion() {
return nativeGetVersion(nativePtr);
}
// FIXME: This should be removed, migratePrimaryKeyTableIfNeeded is using it which should be in Object Store instead?
long getGroupNative() {
return nativeReadGroup(nativePtr);
}
public boolean hasTable(String name) {
return nativeHasTable(nativePtr, name);
}
public Table getTable(String name) {
return new Table(this, nativeGetTable(nativePtr, name));
}
public void renameTable(String oldName, String newName) {
nativeRenameTable(nativePtr, oldName, newName);
}
public void removeTable(String name) {
nativeRemoveTable(nativePtr, name);
}
public String getTableName(int index) {
return nativeGetTableName(nativePtr, index);
}
public long size() {
return nativeSize(nativePtr);
}
public String getPath() {
return configuration.getPath();
}
public boolean isEmpty() {
return nativeIsEmpty(nativePtr);
}
public void refresh() {
nativeRefresh(nativePtr);
invokeSchemaChangeListenerIfSchemaChanged();
}
public SharedRealm.VersionID getVersionID() {
long[] versionId = nativeGetVersionID(nativePtr);
return new SharedRealm.VersionID(versionId[0], versionId[1]);
}
public boolean isClosed() {
return nativeIsClosed(nativePtr);
}
public void writeCopy(File file, byte[] key) {
if (file.isFile() && file.exists()) {
throw new IllegalArgumentException("The destination file must not exist");
}
nativeWriteCopy(nativePtr, file.getAbsolutePath(), key);
}
public boolean waitForChange() {
return nativeWaitForChange(nativePtr);
}
public void stopWaitForChange() {
nativeStopWaitForChange(nativePtr);
}
public boolean compact() {
return nativeCompact(nativePtr);
}
/**
* Updates the underlying schema based on the schema description.
* Calling this method must be done from inside a write transaction.
* <p>
* TODO: This method should not require the caller to get the native pointer.
* Instead, the signature should be something like:
* public <T extends RealmSchema & NativeObject> </T>void updateSchema(T schema, long version)
* ... that is: something that is a schema and that wraps a native object.
*
* @param schemaNativePtr the pointer to a native schema object.
* @param version the target version.
*/
public void updateSchema(long schemaNativePtr, long version) {
nativeUpdateSchema(nativePtr, schemaNativePtr, version);
}
public void setAutoRefresh(boolean enabled) {
capabilities.checkCanDeliverNotification(null);
nativeSetAutoRefresh(nativePtr, enabled);
}
public boolean isAutoRefresh() {
return nativeIsAutoRefresh(nativePtr);
}
/**
* Determine whether the passed schema needs to be updated.
* <p>
* TODO: This method should not require the caller to get the native pointer.
* Instead, the signature should be something like:
* public <T extends RealmSchema & NativeObject> </T>void updateSchema(T schema, long version)
* ... that is, something that is a schema and that wraps a native object.
*
* @param schemaNativePtr the pointer to a native schema object.
* @return true if it will be necessary to call {@code updateSchema}
*/
public boolean requiresMigration(long schemaNativePtr) {
return nativeRequiresMigration(nativePtr, schemaNativePtr);
}
@Override
public void close() {
if (realmNotifier != null) {
realmNotifier.close();
}
synchronized (context) {
nativeCloseSharedRealm(nativePtr);
// Don't reset the nativePtr since we still rely on Object Store to check if the given SharedRealm ptr
// is closed or not.
}
}
@Override
public long getNativePtr() {
return nativePtr;
}
@Override
public long getNativeFinalizerPtr() {
return nativeFinalizerPtr;
}
public void invokeSchemaChangeListenerIfSchemaChanged() {
if (schemaChangeListener == null) {
return;
}
final long before = lastSchemaVersion;
final long current = getSchemaVersion();
if (current != before) {
lastSchemaVersion = current;
schemaChangeListener.onSchemaVersionChanged(current);
}
}
// addIterator(), detachIterators() and invalidateIterators() are used to make RealmResults stable iterators work.
// The iterator will iterate on a snapshot Results if it is accessed inside a transaction.
// See https://github.com/realm/realm-java/issues/3883 for more information.
// Should only be called by Iterator's constructor.
void addIterator(Collection.Iterator iterator) {
iterators.add(new WeakReference<>(iterator));
}
// The detaching should happen before transaction begins.
void detachIterators() {
for (WeakReference<Collection.Iterator> iteratorRef : iterators) {
Collection.Iterator iterator = iteratorRef.get();
if (iterator != null) {
iterator.detach();
}
}
iterators.clear();
}
// Invalidates all iterators when a remote change notification is received.
void invalidateIterators() {
for (WeakReference<Collection.Iterator> iteratorRef : iterators) {
Collection.Iterator iterator = iteratorRef.get();
if (iterator != null) {
iterator.invalidate();
}
}
iterators.clear();
}
// addPendingRow, removePendingRow and executePendingRow queries are to solve that the listener cannot be added
// inside a transaction. For the findFirstAsync(), listener is registered on an Object Store Results first, then move
// the listeners to the Object when the query for Results returns. When beginTransaction() called, all listeners'
// on the results will be triggered first, that leads to the registration of listeners on the Object which will
// throw because of the transaction has already begun. So here we execute all PendingRow queries first before
// calling the Object Store begin_transaction to avoid the problem.
// Add pending row to the list when it is created. It should be called in the PendingRow constructor.
void addPendingRow(PendingRow pendingRow) {
pendingRows.add(new WeakReference<PendingRow>(pendingRow));
}
// Remove pending row from the list. It should be called when pending row's query finished.
void removePendingRow(PendingRow pendingRow) {
for (WeakReference<PendingRow> ref : pendingRows) {
PendingRow row = ref.get();
if (row == null || row == pendingRow) {
pendingRows.remove(ref);
}
}
}
// Execute all pending row queries.
private void executePendingRowQueries() {
for (WeakReference<PendingRow> ref : pendingRows) {
PendingRow row = ref.get();
if (row != null) {
row.executeQuery();
}
}
pendingRows.clear();
}
private static native void nativeInit(String temporaryDirectoryPath);
// Keep last session as an 'object' to avoid any reference to sync code
private static native long nativeCreateConfig(String realmPath, byte[] key, byte schemaMode, boolean inMemory,
boolean cache,
long schemaVersion,
boolean enabledFormatUpgrade,
boolean autoChangeNotification,
String syncServerURL,
String syncServerAuthURL,
String syncUserIdentity,
String syncRefreshToken);
private static native void nativeCloseConfig(long nativeConfigPtr);
private static native long nativeGetSharedRealm(long nativeConfigPtr, RealmNotifier notifier);
private static native void nativeCloseSharedRealm(long nativeSharedRealmPtr);
private static native boolean nativeIsClosed(long nativeSharedRealmPtr);
private static native void nativeBeginTransaction(long nativeSharedRealmPtr);
private static native void nativeCommitTransaction(long nativeSharedRealmPtr);
private static native void nativeCancelTransaction(long nativeSharedRealmPtr);
private static native boolean nativeIsInTransaction(long nativeSharedRealmPtr);
private static native long nativeGetVersion(long nativeSharedRealmPtr);
private static native void nativeSetVersion(long nativeSharedRealmPtr, long version);
private static native long nativeReadGroup(long nativeSharedRealmPtr);
private static native boolean nativeIsEmpty(long nativeSharedRealmPtr);
private static native void nativeRefresh(long nativeSharedRealmPtr);
private static native long[] nativeGetVersionID(long nativeSharedRealmPtr);
private static native long nativeGetTable(long nativeSharedRealmPtr, String tableName);
private static native String nativeGetTableName(long nativeSharedRealmPtr, int index);
private static native boolean nativeHasTable(long nativeSharedRealmPtr, String tableName);
private static native void nativeRenameTable(long nativeSharedRealmPtr, String oldTableName, String newTableName);
private static native void nativeRemoveTable(long nativeSharedRealmPtr, String tableName);
private static native long nativeSize(long nativeSharedRealmPtr);
private static native void nativeWriteCopy(long nativeSharedRealmPtr, String path, byte[] key);
private static native boolean nativeWaitForChange(long nativeSharedRealmPtr);
private static native void nativeStopWaitForChange(long nativeSharedRealmPtr);
private static native boolean nativeCompact(long nativeSharedRealmPtr);
private static native void nativeUpdateSchema(long nativePtr, long nativeSchemaPtr, long version);
private static native void nativeSetAutoRefresh(long nativePtr, boolean enabled);
private static native boolean nativeIsAutoRefresh(long nativePtr);
private static native boolean nativeRequiresMigration(long nativePtr, long nativeSchemaPtr);
private static native long nativeGetFinalizerPtr();
}