All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.objectbox.BoxStore Maven / Gradle / Ivy

There is a newer version: 4.0.3
Show newest version
/*
 * Copyright 2017-2024 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;

import org.greenrobot.essentials.collections.LongHashMap;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

import io.objectbox.annotation.apihint.Beta;
import io.objectbox.annotation.apihint.Experimental;
import io.objectbox.annotation.apihint.Internal;
import io.objectbox.config.DebugFlags;
import io.objectbox.config.FlatStoreOptions;
import io.objectbox.converter.PropertyConverter;
import io.objectbox.exception.DbException;
import io.objectbox.exception.DbExceptionListener;
import io.objectbox.exception.DbSchemaException;
import io.objectbox.internal.Feature;
import io.objectbox.internal.NativeLibraryLoader;
import io.objectbox.internal.ObjectBoxThreadPool;
import io.objectbox.reactive.DataObserver;
import io.objectbox.reactive.DataPublisher;
import io.objectbox.reactive.SubscriptionBuilder;
import io.objectbox.sync.SyncClient;

/**
 * An ObjectBox database that provides {@link Box Boxes} to put and get objects of specific entity classes
 * (see {@link #boxFor(Class)}). To get an instance of this class use {@code MyObjectBox.builder()}.
 */
@SuppressWarnings({"unused", "UnusedReturnValue", "SameParameterValue", "WeakerAccess"})
@ThreadSafe
public class BoxStore implements Closeable {

    /** On Android used for native library loading. */
    @Nullable private static Object context;
    @Nullable private static Object relinker;

    /** Prefix supplied with database directory to signal a file-less and in-memory database should be used. */
    public static final String IN_MEMORY_PREFIX = "memory:";

    /** Change so ReLinker will update native library when using workaround loading. */
    public static final String JNI_VERSION = "4.0.2";

    private static final String VERSION = "4.0.2-2024-08-19";
    private static BoxStore defaultStore;

    /** Currently used DB dirs with values from {@link #getCanonicalPath(File)}. */
    private static final Set openFiles = new HashSet<>();
    private static volatile Thread openFilesCheckerThread;

    @Nullable
    @Internal
    public static synchronized Object getContext() {
        return context;
    }

    @Nullable
    @Internal
    public static synchronized Object getRelinker() {
        return relinker;
    }

    /**
     * Convenience singleton instance which gets set up using {@link BoxStoreBuilder#buildDefault()}.
     * 

* Note: for better testability, you can usually avoid singletons by storing * a {@link BoxStore} instance in some application scope object and pass it along. */ public static synchronized BoxStore getDefault() { if (defaultStore == null) { throw new IllegalStateException("Please call buildDefault() before calling this method"); } return defaultStore; } static synchronized void setDefault(BoxStore store) { if (defaultStore != null) { throw new IllegalStateException("Default store was already built before. "); } defaultStore = store; } /** * Clears the convenience instance. *

* Note: This is usually not required (for testability, please see the comment of {@link #getDefault()}). * * @return true if a default store was available before */ public static synchronized boolean clearDefaultStore() { boolean existedBefore = defaultStore != null; defaultStore = null; return existedBefore; } /** Gets the Version of ObjectBox Java. */ public static String getVersion() { return VERSION; } static native String nativeGetVersion(); /** Gets the Version of ObjectBox Core. */ public static String getVersionNative() { NativeLibraryLoader.ensureLoaded(); return nativeGetVersion(); } /** * @return true if DB files did not exist or were successfully removed, * false if DB files exist that could not be removed. */ static native boolean nativeRemoveDbFiles(String directory, boolean removeDir); /** * Creates a native BoxStore instance with FlatBuffer {@link FlatStoreOptions} {@code options} * and a {@link ModelBuilder} {@code model}. Returns the handle of the native store instance. */ static native long nativeCreateWithFlatOptions(byte[] options, byte[] model); static native boolean nativeIsReadOnly(long store); static native void nativeDelete(long store); static native void nativeDropAllData(long store); /** * A static counter for the alive entity types (entity schema instances); this can be useful to test against leaks. * This number depends on the number of currently opened stores; no matter how often stores were closed and * (re-)opened. E.g. when stores are regularly opened, but not closed by the user, the number should increase. When * all stores are properly closed, this value should be 0. */ @Internal static native long nativeGloballyActiveEntityTypes(); static native long nativeBeginTx(long store); static native long nativeBeginReadTx(long store); /** @return entity ID */ // TODO only use ids once we have them in Java static native int nativeRegisterEntityClass(long store, String entityName, Class entityClass); // TODO only use ids once we have them in Java static native void nativeRegisterCustomType(long store, int entityId, int propertyId, String propertyName, Class converterClass, Class customType); static native String nativeDiagnose(long store); static native int nativeCleanStaleReadTransactions(long store); static native void nativeSetDbExceptionListener(long store, @Nullable DbExceptionListener dbExceptionListener); static native void nativeSetDebugFlags(long store, int debugFlags); private native String nativeStartObjectBrowser(long store, @Nullable String urlPath, int port); private native boolean nativeStopObjectBrowser(long store); static native boolean nativeIsObjectBrowserAvailable(); native long nativeDbSize(long store); native long nativeDbSizeOnDisk(long store); native long nativeValidate(long store, long pageLimit, boolean checkLeafLevel); static native long nativeSysProcMeminfoKb(String key); static native long nativeSysProcStatusKb(String key); private static native boolean nativeHasFeature(int feature); public static boolean hasFeature(Feature feature) { try { NativeLibraryLoader.ensureLoaded(); return nativeHasFeature(feature.id); } catch (UnsatisfiedLinkError e) { System.err.println("Old JNI lib? " + e); // No stack return false; } } public static boolean isObjectBrowserAvailable() { return hasFeature(Feature.ADMIN); } public static boolean isSyncAvailable() { return hasFeature(Feature.SYNC); } public static boolean isSyncServerAvailable() { return hasFeature(Feature.SYNC_SERVER); } native long nativePanicModeRemoveAllObjects(long store, int entityId); private final File directory; private final String canonicalPath; /** Reference to the native store. Should probably get through {@link #getNativeStore()} instead. */ volatile private long handle; private final Map, String> dbNameByClass = new HashMap<>(); private final Map, Integer> entityTypeIdByClass = new HashMap<>(); private final Map, EntityInfo> propertiesByClass = new HashMap<>(); private final LongHashMap> classByEntityTypeId = new LongHashMap<>(); private final int[] allEntityTypeIds; private final Map, Box> boxes = new ConcurrentHashMap<>(); private final Set transactions = Collections.newSetFromMap(new WeakHashMap<>()); private final ExecutorService threadPool = new ObjectBoxThreadPool(this); private final ObjectClassPublisher objectClassPublisher; final boolean debugTxRead; final boolean debugTxWrite; final boolean debugRelations; /** Set when running inside TX */ final ThreadLocal activeTx = new ThreadLocal<>(); // volatile so checkOpen() is more up-to-date (no need for synchronized; it's a race anyway) volatile private boolean closed; final Object txCommitCountLock = new Object(); // Not atomic because it is read most of the time volatile int commitCount; private int objectBrowserPort; private final int queryAttempts; private final TxCallback failedReadTxAttemptCallback; /** * Keeps a reference so the library user does not have to. */ @Nullable private SyncClient syncClient; BoxStore(BoxStoreBuilder builder) { context = builder.context; relinker = builder.relinker; NativeLibraryLoader.ensureLoaded(); directory = builder.directory; canonicalPath = getCanonicalPath(directory); verifyNotAlreadyOpen(canonicalPath); try { handle = nativeCreateWithFlatOptions(builder.buildFlatStoreOptions(canonicalPath), builder.model); if (handle == 0) throw new DbException("Could not create native store"); int debugFlags = builder.debugFlags; if (debugFlags != 0) { debugTxRead = (debugFlags & DebugFlags.LOG_TRANSACTIONS_READ) != 0; debugTxWrite = (debugFlags & DebugFlags.LOG_TRANSACTIONS_WRITE) != 0; } else { debugTxRead = debugTxWrite = false; } debugRelations = builder.debugRelations; for (EntityInfo entityInfo : builder.entityInfoList) { try { dbNameByClass.put(entityInfo.getEntityClass(), entityInfo.getDbName()); int entityId = nativeRegisterEntityClass(handle, entityInfo.getDbName(), entityInfo.getEntityClass()); entityTypeIdByClass.put(entityInfo.getEntityClass(), entityId); classByEntityTypeId.put(entityId, entityInfo.getEntityClass()); propertiesByClass.put(entityInfo.getEntityClass(), entityInfo); for (Property property : entityInfo.getAllProperties()) { if (property.customType != null) { if (property.converterClass == null) { throw new RuntimeException("No converter class for custom type of " + property); } nativeRegisterCustomType(handle, entityId, 0, property.dbName, property.converterClass, property.customType); } } } catch (RuntimeException e) { throw new RuntimeException("Could not setup up entity " + entityInfo.getEntityClass(), e); } } int size = classByEntityTypeId.size(); allEntityTypeIds = new int[size]; long[] entityIdsLong = classByEntityTypeId.keys(); for (int i = 0; i < size; i++) { allEntityTypeIds[i] = (int) entityIdsLong[i]; } objectClassPublisher = new ObjectClassPublisher(this); failedReadTxAttemptCallback = builder.failedReadTxAttemptCallback; queryAttempts = Math.max(builder.queryAttempts, 1); } catch (RuntimeException runtimeException) { close(); // Proper clean up, e.g. delete native handle, remove this path from openFiles throw runtimeException; } } static String getCanonicalPath(File directory) { // Skip directory check if in-memory prefix is used. if (directory.getPath().startsWith(IN_MEMORY_PREFIX)) { // Just return the path as is (e.g. "memory:data"), safe to use for string-based open check as well. return directory.getPath(); } if (directory.exists()) { if (!directory.isDirectory()) { throw new DbException("Is not a directory: " + directory.getAbsolutePath()); } } else if (!directory.mkdirs()) { throw new DbException("Could not create directory: " + directory.getAbsolutePath()); } try { return directory.getCanonicalPath(); } catch (IOException e) { throw new DbException("Could not verify dir", e); } } static void verifyNotAlreadyOpen(String canonicalPath) { synchronized (openFiles) { isFileOpen(canonicalPath); // for retries if (!openFiles.add(canonicalPath)) { throw new DbException("Another BoxStore is still open for this directory: " + canonicalPath + ". Hint: for most apps it's recommended to keep a BoxStore for the app's life time."); } } } /** Also retries up to 500ms to improve GC race condition situation. */ static boolean isFileOpen(final String canonicalPath) { synchronized (openFiles) { if (!openFiles.contains(canonicalPath)) return false; } Thread checkerThread = BoxStore.openFilesCheckerThread; if (checkerThread == null || !checkerThread.isAlive()) { // Use a thread to avoid finalizers that block us checkerThread = new Thread(() -> { isFileOpenSync(canonicalPath, true); BoxStore.openFilesCheckerThread = null; // Clean ref to itself }); checkerThread.setDaemon(true); BoxStore.openFilesCheckerThread = checkerThread; checkerThread.start(); try { checkerThread.join(500); } catch (InterruptedException e) { e.printStackTrace(); } } else { // Waiting for finalizers are blocking; only do that in the thread ^ return isFileOpenSync(canonicalPath, false); } synchronized (openFiles) { return openFiles.contains(canonicalPath); } } static boolean isFileOpenSync(String canonicalPath, boolean runFinalization) { synchronized (openFiles) { int tries = 0; while (tries < 5 && openFiles.contains(canonicalPath)) { tries++; System.gc(); if (runFinalization && tries > 1) System.runFinalization(); System.gc(); if (runFinalization && tries > 1) System.runFinalization(); try { openFiles.wait(100); } catch (InterruptedException e) { // Ignore } } return openFiles.contains(canonicalPath); } } /** * Using an Android Context and an optional database name, as configured with {@link BoxStoreBuilder#name(String)}, * checks if the associated database files are in use by a BoxStore instance. *

* Use this to check that database files are not open before copying or deleting them. */ public static boolean isDatabaseOpen(Object context, @Nullable String dbNameOrNull) throws IOException { File dbDir = BoxStoreBuilder.getAndroidDbDir(context, dbNameOrNull); return isFileOpen(dbDir.getCanonicalPath()); } /** * Using an optional base directory, as configured with {@link BoxStoreBuilder#baseDirectory(File)}, * and an optional database name, as configured with {@link BoxStoreBuilder#name(String)}, * checks if the associated database files are in use by a BoxStore instance. *

* Use this to check that database files are not open before copying or deleting them. */ public static boolean isDatabaseOpen(@Nullable File baseDirectoryOrNull, @Nullable String dbNameOrNull) throws IOException { File dbDir = BoxStoreBuilder.getDbDir(baseDirectoryOrNull, dbNameOrNull); return isFileOpen(dbDir.getCanonicalPath()); } /** * Using a directory, as configured with {@link BoxStoreBuilder#directory(File)}, * checks if the associated database files are in use by a BoxStore instance. *

* Use this to check that database files are not open before copying or deleting them. */ public static boolean isDatabaseOpen(File directory) throws IOException { return isFileOpen(directory.getCanonicalPath()); } /** * Linux only: extracts a kB value from /proc/meminfo (system wide memory information). * A couple of interesting keys (from 'man proc'): * - MemTotal: Total usable RAM (i.e., physical RAM minus a few reserved bits and the kernel binary code). * - MemFree: The sum of LowFree+HighFree. * - MemAvailable: An estimate of how much memory is available for starting new applications, without swapping. * * @param key The string identifying the wanted line from /proc/meminfo to extract a Kb value from. E.g. "MemTotal". * @return Kb value or 0 on failure */ @Experimental public static long sysProcMeminfoKb(String key) { NativeLibraryLoader.ensureLoaded(); return nativeSysProcMeminfoKb(key); } /** * Linux only: extracts a kB value from /proc/self/status (process specific information). * A couple of interesting keys (from 'man proc'): * - VmPeak: Peak virtual memory size. * - VmSize: Virtual memory size. * - VmHWM: Peak resident set size ("high water mark"). * - VmRSS: Resident set size. Note that the value here is the sum of RssAnon, RssFile, and RssShmem. * - RssAnon: Size of resident anonymous memory. (since Linux 4.5). * - RssFile: Size of resident file mappings. (since Linux 4.5). * - RssShmem: Size of resident shared memory (includes System V shared memory, mappings from tmpfs(5), * and shared anonymous mappings). (since Linux 4.5). * - VmData, VmStk, VmExe: Size of data, stack, and text segments. * - VmLib: Shared library code size. * * @param key The string identifying the wanted line from /proc/self/status to extract a Kb value from. E.g. "VmSize". * @return Kb value or 0 on failure */ @Experimental public static long sysProcStatusKb(String key) { NativeLibraryLoader.ensureLoaded(); return nativeSysProcStatusKb(key); } /** * The size in bytes occupied by the data file on disk. * * @return 0 if the size could not be determined (does not throw unless this store was already closed) * @deprecated Use {@link #getDbSize()} or {@link #getDbSizeOnDisk()} instead which properly handle in-memory databases. */ @Deprecated public long sizeOnDisk() { return getDbSize(); } /** * Get the size of this store. For a disk-based store type, this corresponds to the size on disk, and for the * in-memory store type, this is roughly the used memory bytes occupied by the data. * * @return The size in bytes of the database, or 0 if the file does not exist or some error occurred. */ public long getDbSize() { return nativeDbSize(getNativeStore()); } /** * The size in bytes occupied by the database on disk (if any). * * @return The size in bytes of the database on disk, or 0 if the underlying database is in-memory only * or the size could not be determined. */ public long getDbSizeOnDisk() { return nativeDbSizeOnDisk(getNativeStore()); } /** * Closes this if this is finalized. *

* Explicitly call {@link #close()} instead to avoid expensive finalization. */ @SuppressWarnings("deprecation") // finalize() @Override protected void finalize() throws Throwable { close(); super.finalize(); } /** * Verifies this has not been {@link #close() closed}. */ private void checkOpen() { if (isClosed()) { throw new IllegalStateException("Store is closed"); } } String getDbName(Class entityClass) { return dbNameByClass.get(entityClass); } Integer getEntityTypeId(Class entityClass) { return entityTypeIdByClass.get(entityClass); } @Internal public int getEntityTypeIdOrThrow(Class entityClass) { Integer id = entityTypeIdByClass.get(entityClass); if (id == null) { throw new DbSchemaException("No entity registered for " + entityClass); } return id; } public Collection> getAllEntityClasses() { return dbNameByClass.keySet(); } @Internal int[] getAllEntityTypeIds() { return allEntityTypeIds; } @Internal Class getEntityClassOrThrow(int entityTypeId) { Class clazz = classByEntityTypeId.get(entityTypeId); if (clazz == null) { throw new DbSchemaException("No entity registered for type ID " + entityTypeId); } return clazz; } @SuppressWarnings("unchecked") // Casting is easier than writing a custom Map. @Internal EntityInfo getEntityInfo(Class entityClass) { return (EntityInfo) propertiesByClass.get(entityClass); } /** * Internal, low level method: use {@link #runInTx(Runnable)} instead. */ @Internal public Transaction beginTx() { // Because write TXs are typically not cached, initialCommitCount is not as relevant than for read TXs. int initialCommitCount = commitCount; if (debugTxWrite) { System.out.println("Begin TX with commit count " + initialCommitCount); } long nativeTx = nativeBeginTx(getNativeStore()); if (nativeTx == 0) throw new DbException("Could not create native transaction"); Transaction tx = new Transaction(this, nativeTx, initialCommitCount); synchronized (transactions) { transactions.add(tx); } return tx; } /** * Internal, low level method: use {@link #runInReadTx(Runnable)} instead. * Begins a transaction for read access only. Note: there may be only one read transaction per thread. */ @Internal public Transaction beginReadTx() { // initialCommitCount should be acquired before starting the tx. In race conditions, there is a chance the // commitCount is already outdated. That's OK because it only gives a false positive for an TX being obsolete. // In contrast, a false negative would make a TX falsely not considered obsolete, and thus readers would not be // updated resulting in querying obsolete data until another commit is done. // TODO add multithreaded test for this int initialCommitCount = commitCount; if (debugTxRead) { System.out.println("Begin read TX with commit count " + initialCommitCount); } long nativeTx = nativeBeginReadTx(getNativeStore()); if (nativeTx == 0) throw new DbException("Could not create native read transaction"); Transaction tx = new Transaction(this, nativeTx, initialCommitCount); synchronized (transactions) { transactions.add(tx); } return tx; } /** * If this was {@link #close() closed}. */ public boolean isClosed() { return closed; } /** * Whether the store was created using read-only mode. * If true the schema is not updated and write transactions are not possible. */ public boolean isReadOnly() { return nativeIsReadOnly(getNativeStore()); } /** * Closes this BoxStore and releases associated resources. *

* Before calling, all database operations must have finished (there are no more active transactions). *

* If that is not the case, the method will briefly wait on any active transactions, but then will forcefully close * them to avoid crashes and print warning messages ("Transactions are still active"). If this occurs, * analyze your code to make sure all database operations, notably in other threads or data observers, * are properly finished. */ public void close() { boolean oldClosedState; synchronized (this) { oldClosedState = closed; if (!closed) { if (objectBrowserPort != 0) { // not linked natively (yet), so clean up here try { stopObjectBrowser(); } catch (Throwable e) { e.printStackTrace(); } } // Closeable recommendation: mark as closed before any code that might throw. // Also, before checking on transactions to avoid any new transactions from getting created // (due to all Java APIs doing closed checks). closed = true; List transactionsToClose; synchronized (transactions) { // Give open transactions some time to close (BoxStore.unregisterTransaction() calls notify), // 1000 ms should be long enough for most small operations and short enough to avoid ANRs on Android. if (hasActiveTransaction()) { System.out.println("Briefly waiting for active transactions before closing the Store..."); try { // It is fine to hold a lock on BoxStore.this as well as BoxStore.unregisterTransaction() // only synchronizes on "transactions". //noinspection WaitWhileHoldingTwoLocks transactions.wait(1000); } catch (InterruptedException e) { // If interrupted, continue with releasing native resources } if (hasActiveTransaction()) { System.err.println("Transactions are still active:" + " ensure that all database operations are finished before closing the Store!"); } } transactionsToClose = new ArrayList<>(this.transactions); } // Close all transactions, including recycled (not active) ones stored in Box threadLocalReader. // It is expected that this prints a warning if a transaction is not owned by the current thread. for (Transaction t : transactionsToClose) { t.close(); } long handleToDelete = handle; // Make isNativeStoreClosed() return true before actually closing to avoid Transaction.close() crash handle = 0; if (handleToDelete != 0) { // failed before native handle was created? nativeDelete(handleToDelete); } // When running the full unit test suite, we had 100+ threads before, hope this helps: threadPool.shutdown(); checkThreadTermination(); } } if (!oldClosedState) { synchronized (openFiles) { openFiles.remove(canonicalPath); openFiles.notifyAll(); } } } /** dump thread stacks if pool does not terminate promptly. */ private void checkThreadTermination() { try { if (!threadPool.awaitTermination(1, TimeUnit.SECONDS)) { int activeCount = Thread.activeCount(); System.err.println("Thread pool not terminated in time; printing stack traces..."); Thread[] threads = new Thread[activeCount + 2]; int count = Thread.enumerate(threads); for (int i = 0; i < count; i++) { System.err.println("Thread: " + threads[i].getName()); Thread.dumpStack(); } } } catch (InterruptedException e) { e.printStackTrace(); } } /** * Danger zone! This will delete all data (files) of this BoxStore! * You must call {@link #close()} before and read the docs of that method carefully! *

* A safer alternative: use the static {@link #deleteAllFiles(File)} method before opening the BoxStore. * * @return true if the directory 1) was deleted successfully OR 2) did not exist in the first place. * Note: If false is returned, any number of files may have been deleted before the failure happened. */ public boolean deleteAllFiles() { if (!isClosed()) { throw new IllegalStateException("Store must be closed"); } return deleteAllFiles(directory); } /** * Danger zone! This will delete all files in the given directory! *

* No {@link BoxStore} may be alive using the given directory. E.g. call this before building a store. When calling * this after {@link #close() closing} a store, read the docs of that method carefully first! *

* If no {@link BoxStoreBuilder#name(String) name} was specified when building the store, use like: * *

{@code
     *     BoxStore.deleteAllFiles(new File(BoxStoreBuilder.DEFAULT_NAME));
     * }
* *

For an {@link BoxStoreBuilder#inMemory(String) in-memory} database, this will just clean up the in-memory * database. * * @param objectStoreDirectory directory to be deleted; this is the value you previously provided to {@link * BoxStoreBuilder#directory(File)} * @return true if the directory 1) was deleted successfully OR 2) did not exist in the first place. * Note: If false is returned, any number of files may have been deleted before the failure happened. * @throws IllegalStateException if the given directory is still used by an open {@link BoxStore}. */ public static boolean deleteAllFiles(File objectStoreDirectory) { String canonicalPath = getCanonicalPath(objectStoreDirectory); if (isFileOpen(canonicalPath)) { throw new IllegalStateException("Cannot delete files: store is still open"); } NativeLibraryLoader.ensureLoaded(); return nativeRemoveDbFiles(canonicalPath, true); } /** * Danger zone! This will delete all files in the given directory! *

* No {@link BoxStore} may be alive using the given name. *

* If you did not use a custom name with BoxStoreBuilder, you can pass "new File({@link * BoxStoreBuilder#DEFAULT_NAME})". * * @param androidContext provide an Android Context like Application or Service * @param customDbNameOrNull use null for default name, or the name you previously provided to {@link * BoxStoreBuilder#name(String)}. * @return true if the directory 1) was deleted successfully OR 2) did not exist in the first place. * Note: If false is returned, any number of files may have been deleted before the failure happened. * @throws IllegalStateException if the given name is still used by a open {@link BoxStore}. */ public static boolean deleteAllFiles(Object androidContext, @Nullable String customDbNameOrNull) { File dbDir = BoxStoreBuilder.getAndroidDbDir(androidContext, customDbNameOrNull); return deleteAllFiles(dbDir); } /** * Danger zone! This will delete all files in the given directory! *

* No {@link BoxStore} may be alive using the given directory. *

* If you did not use a custom name with BoxStoreBuilder, you can pass "new File({@link * BoxStoreBuilder#DEFAULT_NAME})". * * @param baseDirectoryOrNull use null for no base dir, or the value you previously provided to {@link * BoxStoreBuilder#baseDirectory(File)} * @param customDbNameOrNull use null for default name, or the name you previously provided to {@link * BoxStoreBuilder#name(String)}. * @return true if the directory 1) was deleted successfully OR 2) did not exist in the first place. * Note: If false is returned, any number of files may have been deleted before the failure happened. * @throws IllegalStateException if the given directory (+name) is still used by a open {@link BoxStore}. */ public static boolean deleteAllFiles(@Nullable File baseDirectoryOrNull, @Nullable String customDbNameOrNull) { File dbDir = BoxStoreBuilder.getDbDir(baseDirectoryOrNull, customDbNameOrNull); return deleteAllFiles(dbDir); } /** * Removes all objects from all types ("boxes"), e.g. deletes all database content * (excluding meta data like the data model). * This typically performs very quickly (e.g. faster than {@link Box#removeAll()}). *

* Note that this does not reclaim disk space: the already reserved space for the DB file(s) is used in the future * resulting in better performance because no/less disk allocation has to be done. *

* If you want to reclaim disk space, delete the DB file(s) instead: *

    *
  • {@link #close()} the BoxStore (and ensure that no thread access it)
  • *
  • {@link #deleteAllFiles()} of the BoxStore
  • *
  • Open a new BoxStore
  • *
*/ public void removeAllObjects() { nativeDropAllData(getNativeStore()); } @Internal public void unregisterTransaction(Transaction transaction) { synchronized (transactions) { transactions.remove(transaction); // For close(): notify if there are no more open transactions if (!hasActiveTransaction()) { transactions.notifyAll(); } } } /** * Returns if {@link #transactions} has a single transaction that {@link Transaction#isActive() isActive()}. *

* Callers must synchronize on {@link #transactions}. */ private boolean hasActiveTransaction() { for (Transaction tx : transactions) { if (tx.isActive()) { return true; } } return false; } void txCommitted(Transaction tx, @Nullable int[] entityTypeIdsAffected) { // Only one write TX at a time, but there is a chance two writers race after commit: thus synchronize synchronized (txCommitCountLock) { commitCount++; // Overflow is OK because we check for equality if (debugTxWrite) { System.out.println("TX committed. New commit count: " + commitCount + ", entity types affected: " + (entityTypeIdsAffected != null ? entityTypeIdsAffected.length : 0)); } } for (Box box : boxes.values()) { box.txCommitted(tx); } if (entityTypeIdsAffected != null) { objectClassPublisher.publish(entityTypeIdsAffected); } } /** * Returns a Box for the given type. Objects are put into (and get from) their individual Box. *

* Creates a Box only once and then always returns the cached instance. */ @SuppressWarnings("unchecked") // Casting is easier than writing a custom Map. public Box boxFor(Class entityClass) { Box box = (Box) boxes.get(entityClass); if (box == null) { if (!dbNameByClass.containsKey(entityClass)) { throw new IllegalArgumentException(entityClass + " is not a known entity. Please add it and trigger generation again."); } // Ensure a box is created just once synchronized (boxes) { box = (Box) boxes.get(entityClass); if (box == null) { box = new Box<>(this, entityClass); boxes.put(entityClass, box); } } } return box; } /** * Runs the given runnable inside a transaction. *

* Efficiency notes: it is advised to run multiple puts in a transaction because each commit requires an expensive * disk synchronization. */ public void runInTx(Runnable runnable) { Transaction tx = activeTx.get(); // Only if not already set, allowing to call it recursively with first (outer) TX if (tx == null) { tx = beginTx(); activeTx.set(tx); try { runnable.run(); tx.commit(); } finally { activeTx.remove(); tx.close(); } } else { if (tx.isReadOnly()) { throw new IllegalStateException("Cannot start a transaction while a read only transaction is active"); } runnable.run(); } } /** * Runs the given runnable inside a read(-only) transaction. Multiple read transactions can occur at the same time. * This allows multiple read operations (gets) using a single consistent state of data. * Also, for a high number of read operations (thousands, e.g. in loops), * it is advised to run them in a single read transaction for efficiency reasons. */ public void runInReadTx(Runnable runnable) { Transaction tx = activeTx.get(); // Only if not already set, allowing to call it recursively with first (outer) TX if (tx == null) { tx = beginReadTx(); activeTx.set(tx); try { runnable.run(); } finally { activeTx.remove(); // TODO That's rather a quick fix, replace with a more general solution // (that could maybe be a TX listener with abort callback?) for (Box box : boxes.values()) { box.readTxFinished(tx); } tx.close(); } } else { runnable.run(); } } /** * Calls {@link #callInReadTx(Callable)} and retries in case a DbException is thrown. * If the given amount of attempts is reached, the last DbException will be thrown. * Experimental: API might change. */ @Experimental public T callInReadTxWithRetry(Callable callable, int attempts, int initialBackOffInMs, boolean logAndHeal) { if (attempts == 1) { return callInReadTx(callable); } else if (attempts < 1) { throw new IllegalArgumentException("Illegal value of attempts: " + attempts); } long backoffInMs = initialBackOffInMs; DbException lastException = null; for (int attempt = 1; attempt <= attempts; attempt++) { try { return callInReadTx(callable); } catch (DbException e) { lastException = e; String diagnose = diagnose(); String message = attempt + " of " + attempts + " attempts of calling a read TX failed:"; if (logAndHeal) { System.err.println(message); e.printStackTrace(); System.err.println(diagnose); System.err.flush(); System.gc(); System.runFinalization(); cleanStaleReadTransactions(); } if (failedReadTxAttemptCallback != null) { failedReadTxAttemptCallback.txFinished(null, new DbException(message + " \n" + diagnose, e)); } try { Thread.sleep(backoffInMs); } catch (InterruptedException ie) { ie.printStackTrace(); throw lastException; } backoffInMs *= 2; } } throw lastException; } /** * Calls the given callable inside a read(-only) transaction. Multiple read transactions can occur at the same time. * This allows multiple read operations (gets) using a single consistent state of data. * Also, for a high number of read operations (thousands, e.g. in loops), * it is advised to run them in a single read transaction for efficiency reasons. * Note that an exception thrown by the given Callable will be wrapped in a RuntimeException, if the exception is * not a RuntimeException itself. */ public T callInReadTx(Callable callable) { Transaction tx = activeTx.get(); // Only if not already set, allowing to call it recursively with first (outer) TX if (tx == null) { tx = beginReadTx(); activeTx.set(tx); try { return callable.call(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException("Callable threw exception", e); } finally { activeTx.remove(); // TODO That's rather a quick fix, replace with a more general solution // (that could maybe be a TX listener with abort callback?) for (Box box : boxes.values()) { box.readTxFinished(tx); } tx.close(); } } else { try { return callable.call(); } catch (Exception e) { throw new RuntimeException("Callable threw exception", e); } } } /** * Like {@link #runInTx(Runnable)}, but allows returning a value and throwing an exception. */ public R callInTx(Callable callable) throws Exception { Transaction tx = activeTx.get(); // Only if not already set, allowing to call it recursively with first (outer) TX if (tx == null) { tx = beginTx(); activeTx.set(tx); try { R result = callable.call(); tx.commit(); return result; } finally { activeTx.remove(); tx.close(); } } else { if (tx.isReadOnly()) { throw new IllegalStateException("Cannot start a transaction while a read only transaction is active"); } return callable.call(); } } /** * Like {@link #callInTx(Callable)}, but throws no Exception. * Any Exception thrown in the Callable is wrapped in a RuntimeException. */ public R callInTxNoException(Callable callable) { try { return callInTx(callable); } catch (Exception e) { throw new RuntimeException(e); } } /** * Runs the given Runnable as a transaction in a separate thread. * Once the transaction completes the given callback is called (callback may be null). *

* See also {@link #runInTx(Runnable)}. */ public void runInTxAsync(final Runnable runnable, @Nullable final TxCallback callback) { threadPool.submit(() -> { try { runInTx(runnable); if (callback != null) { callback.txFinished(null, null); } } catch (Throwable failure) { if (callback != null) { callback.txFinished(null, failure); } } }); } /** * Runs the given Runnable as a transaction in a separate thread. * Once the transaction completes the given callback is called (callback may be null). *

* * See also {@link #callInTx(Callable)}. */ public void callInTxAsync(final Callable callable, @Nullable final TxCallback callback) { threadPool.submit(() -> { try { R result = callInTx(callable); if (callback != null) { callback.txFinished(result, null); } } catch (Throwable failure) { if (callback != null) { callback.txFinished(null, failure); } } }); } /** * Gives info that can be useful for debugging. * * @return String that is typically logged by the application. */ public String diagnose() { return nativeDiagnose(getNativeStore()); } /** * Validate database pages, a lower level storage unit (integrity check). * Do not call this inside a transaction (currently unsupported). * * @param pageLimit the maximum of pages to validate (e.g. to limit time spent on validation). * Pass zero set no limit and thus validate all pages. * @param checkLeafLevel Flag to validate leaf pages. These do not point to other pages but contain data. * @return Number of pages validated, which may be twice the given pageLimit as internally there are "two DBs". * @throws DbException if validation failed to run (does not tell anything about DB file consistency). * @throws io.objectbox.exception.FileCorruptException if the DB file is actually inconsistent (corrupt). */ @Beta public long validate(long pageLimit, boolean checkLeafLevel) { if (pageLimit < 0) { throw new IllegalArgumentException("pageLimit must be zero or positive"); } return nativeValidate(getNativeStore(), pageLimit, checkLeafLevel); } public int cleanStaleReadTransactions() { return nativeCleanStaleReadTransactions(getNativeStore()); } /** * Call this method from a thread that is about to be shutdown or likely not to use ObjectBox anymore: * it frees any cached resources tied to the calling thread (e.g. readers). This method calls * {@link Box#closeThreadResources()} for all initiated boxes ({@link #boxFor(Class)}). */ public void closeThreadResources() { for (Box box : boxes.values()) { box.closeThreadResources(); } // activeTx is cleaned up in finally blocks, so do not free them here } /** * A {@link io.objectbox.reactive.DataObserver} can be subscribed to data changes using the returned builder. * The observer is supplied via {@link SubscriptionBuilder#observer(DataObserver)} and will be notified once a * transaction is committed and will receive changes to any object class. *

* Threading notes: * All observers are notified from one separate thread (pooled). Observers are not notified in parallel. * The notification order is the same as the subscription order, although this may not always be guaranteed in * the future. *

* Note that failed or aborted transaction do not trigger observers. */ public SubscriptionBuilder subscribe() { checkOpen(); return new SubscriptionBuilder<>(objectClassPublisher, null); } /** * Like {@link #subscribe()}, but wires the supplied @{@link io.objectbox.reactive.DataObserver} only to the given * object class for notifications. */ @SuppressWarnings("unchecked") public SubscriptionBuilder> subscribe(Class forClass) { checkOpen(); return new SubscriptionBuilder<>((DataPublisher) objectClassPublisher, forClass); } @Experimental @Nullable public String startObjectBrowser() { verifyObjectBrowserNotRunning(); final int basePort = 8090; for (int port = basePort; port < basePort + 10; port++) { try { String url = startObjectBrowser(port); if (url != null) { return url; } } catch (DbException e) { if (e.getMessage() == null || !e.getMessage().contains("port")) { throw e; } } } return null; } @Experimental @Nullable public String startObjectBrowser(int port) { verifyObjectBrowserNotRunning(); String url = nativeStartObjectBrowser(getNativeStore(), null, port); if (url != null) { objectBrowserPort = port; } return url; } @Experimental @Nullable public String startObjectBrowser(String urlToBindTo) { verifyObjectBrowserNotRunning(); int port; try { port = new URL(urlToBindTo).getPort(); // Gives -1 if not available } catch (MalformedURLException e) { throw new RuntimeException("Can not start Object Browser at " + urlToBindTo, e); } String url = nativeStartObjectBrowser(getNativeStore(), urlToBindTo, 0); if (url != null) { objectBrowserPort = port; } return url; } @Experimental public synchronized boolean stopObjectBrowser() { if (objectBrowserPort == 0) { throw new IllegalStateException("ObjectBrowser has not been started before"); } objectBrowserPort = 0; return nativeStopObjectBrowser(getNativeStore()); } @Experimental public int getObjectBrowserPort() { return objectBrowserPort; } public boolean isObjectBrowserRunning() { return objectBrowserPort != 0; } private void verifyObjectBrowserNotRunning() { if (objectBrowserPort != 0) { throw new DbException("ObjectBrowser is already running at port " + objectBrowserPort); } } /** * Sets a listener that will be called when an exception is thrown. Replaces a previously set listener. * Set to {@code null} to remove the listener. *

* This for example allows central error handling or special logging for database-related exceptions. */ public void setDbExceptionListener(@Nullable DbExceptionListener dbExceptionListener) { nativeSetDbExceptionListener(getNativeStore(), dbExceptionListener); } @Internal public Future internalScheduleThread(Runnable runnable) { return threadPool.submit(runnable); } @Internal public ExecutorService internalThreadPool() { return threadPool; } @Internal public boolean isDebugRelations() { return debugRelations; } @Internal public int internalQueryAttempts() { return queryAttempts; } @Internal public TxCallback internalFailedReadTxAttemptCallback() { return failedReadTxAttemptCallback; } void setDebugFlags(int debugFlags) { nativeSetDebugFlags(getNativeStore(), debugFlags); } long panicModeRemoveAllObjects(int entityId) { return nativePanicModeRemoveAllObjects(getNativeStore(), entityId); } /** * Gets the reference to the native store. Can be used with the C API to use the same store, e.g. via JNI, by * passing it on to {@code obx_store_wrap()}. *

* Throws if the store is closed. *

* The procedure is like this:
* 1) you create a BoxStore on the Java side
* 2) you call this method to get the native store pointer
* 3) you pass the native store pointer to your native code (e.g. via JNI)
* 4) your native code calls obx_store_wrap() with the native store pointer to get a OBX_store pointer
* 5) Using the OBX_store pointer, you can use the C API. *

* Note: Once you {@link #close()} this BoxStore, do not use it from the C API. */ public long getNativeStore() { checkOpen(); return handle; } /** * For internal use only. This API might change or be removed with a future release. *

* Returns if the native Store was closed. *

* This is {@code true} shortly after {@link #close()} was called and {@link #isClosed()} returns {@code true}. */ @Internal public boolean isNativeStoreClosed() { return handle == 0; } /** * Returns the {@link SyncClient} associated with this store. To create one see {@link io.objectbox.sync.Sync Sync}. */ @Nullable public SyncClient getSyncClient() { return syncClient; } void setSyncClient(@Nullable SyncClient syncClient) { this.syncClient = syncClient; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy