com.abubusoft.kripton.android.sqlite.AbstractDataSource Maven / Gradle / Ivy
Show all versions of kripton-orm Show documentation
/**
* Copyright 2015, 2017 Francesco Benincasa ([email protected]).
*
* 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 com.abubusoft.kripton.android.sqlite;
import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder;
import androidx.sqlite.db.SupportSQLiteStatement;
import com.abubusoft.kripton.android.KriptonLibrary;
import com.abubusoft.kripton.android.Logger;
import com.abubusoft.kripton.common.Pair;
import com.abubusoft.kripton.common.StringUtils;
import com.abubusoft.kripton.exception.KriptonRuntimeException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
*
* Base class for data source
*
* .
*
* @author Francesco Benincasa ([email protected])
*/
public abstract class AbstractDataSource implements AutoCloseable {
/**
* Interface for database operations.
*
* @param the element type
*/
public interface AbstractExecutable {
/**
* Execute transation. Method need to return
* {@link TransactionResult#COMMIT} to commit results or
* {@link TransactionResult#ROLLBACK} to rollback. If exception is
* thrown, a rollback will be done.
*
* @param daoFactory the dao factory
* @return the transaction result
*/
TransactionResult onExecute(@NonNull E daoFactory);
}
/**
* The listener interface for receiving onError events. The class that is
* interested in processing a onError event implements this interface, and
* the object created with that class is registered with a component using
* the component's addOnErrorListener
method. When the onError
* event occurs, that object's appropriate method is invoked.
*/
public interface OnErrorListener {
/**
* Manages error situations.
*
* @param e exception
*/
void onError(@NonNull Throwable e);
}
/**
* The Enum TypeStatus.
*/
public enum TypeStatus {
/**
* The closed.
*/
CLOSED,
/**
* The read and write opened.
*/
READ_AND_WRITE_OPENED,
/**
* The read only opened.
*/
READ_ONLY_OPENED
}
/**
* The context.
*/
protected SQLContextImpl context;
/**
* database instance.
*/
SupportSQLiteDatabase database;
/**
*
* True if dataSource is just created
*
* .
*/
protected boolean justCreated = false;
/**
* The lock access.
*/
private final ReentrantReadWriteLock lockAccess = new ReentrantReadWriteLock();
/**
* The lock db.
*/
private final ReentrantLock lockDb = new ReentrantLock();
/**
* The lock read access.
*/
private final Lock lockReadAccess = lockAccess.readLock();
/**
* The lock read write access.
*/
private final Lock lockReadWriteAccess = lockAccess.writeLock();
/**
* The log enabled.
*/
protected boolean logEnabled;
/**
*
* file name used to save database,
*
* .
*/
protected final String name;
/**
* The on error listener.
*/
protected OnErrorListener onErrorListener = (Throwable e) -> {
throw (new KriptonRuntimeException(e));
};
/**
* The open counter.
*/
private final AtomicInteger openCounter = new AtomicInteger();
/**
* The options.
*/
protected DataSourceOptions options;
/**
* The sqlite helper.
*/
protected SupportSQLiteOpenHelper sqliteHelper;
/**
* The status. Do not replace with Initial, it does not work on Android.
*/
protected ThreadLocal status = new ThreadLocal() {
@Override
protected TypeStatus initialValue() {
return TypeStatus.CLOSED;
}
};
/**
*
* database version
*
* .
*/
protected int version;
/**
* if true, database was update during this application run.
*/
protected boolean versionChanged;
/**
* Instantiates a new abstract data source.
*
* @param name the name
* @param version the version
* @param options the options
*/
protected AbstractDataSource(String name, int version, DataSourceOptions options) {
DataSourceOptions optionsValue = (options != null) ? options : DataSourceOptions.builder().build();
if (optionsValue.inMemory) {
this.name = null;
} else if (StringUtils.hasText(optionsValue.name)) {
this.name = optionsValue.name;
} else {
this.name = name;
}
this.version = version;
// create new SQLContext
this.context = new SQLContextImpl(this);
this.options = optionsValue;
this.logEnabled = optionsValue.logEnabled;
if (this.logEnabled) {
Logger.debug("%s is created with %s", getClass().getName(), optionsValue.toString());
}
}
protected void beginLock() {
lockDb.lock();
}
/**
* Builds the task list.
*
* @param previousVersion the previous version
* @param currentVersion the current version
* @return the list
*/
protected List buildTaskList(int previousVersion, int currentVersion) {
List result = new ArrayList<>();
for (Pair item : this.options.updateTasks) {
if (item.value0 - 1 == previousVersion) {
result.add(item.value1);
previousVersion = item.value0;
}
if (previousVersion == currentVersion)
break;
}
if (previousVersion != currentVersion) {
Logger.warn(String.format("Can not find version update task from version %s to version %s", previousVersion,
currentVersion));
}
return result;
}
/**
* used to clear prepared statements.
*/
public abstract void clearCompiledStatements();
/**
* Context.
*
* @return the SQL context
*/
public SQLContext getContext() {
return context;
}
/*
* (non-Javadoc)
*
* @see android.database.sqlite.SQLiteOpenHelper#close()
*/
@Override
public void close() {
beginLock();
try {
if (openCounter.decrementAndGet() <= 0) {
if (options.neverClose) {
if (logEnabled)
Logger.info("database %s VIRTUALLY CLOSED (%s) (connections: %s)", getDataSourceSafeName(), status.get(), openCounter.intValue());
} else {
if (!this.options.inMemory) {
// Closing database
if (database != null) {
clearCompiledStatements();
sqliteHelper.close();
}
database = null;
}
if (logEnabled)
Logger.info("database %s CLOSED (%s) (connections: %s)", getDataSourceSafeName(), status.get(), openCounter.intValue());
}
} else {
if (logEnabled)
Logger.info("database %s VIRTUALLY RELEASED (%s) (connections: %s)", getDataSourceSafeName(), status.get(), openCounter.intValue());
}
} catch (Exception e) {
e.printStackTrace();
throw (e);
} finally {
manageStatus();
endLock();
}
}
private String getDataSourceSafeName() {
return name != null ? name : "in memory";
}
protected void closeThreadSafeMode(Pair status) {
if (status.value0) {
close();
} else {
beginLock();
// we unlock lockReadWriteAccess, so we can include this code in
manageStatus();
endLock();
}
}
/**
* Content values.
*
* @param compiledStatement the compiled statement
* @return the kripton content values
*/
protected KriptonContentValues contentValues(SupportSQLiteStatement compiledStatement) {
return context.contentValues(compiledStatement);
}
/**
* Content values for content provider.
*
* @param values the values
* @return the kripton content values
*/
protected KriptonContentValues contentValuesForContentProvider(ContentValues values) {
return context.contentValuesForContentProvider(values);
}
/**
* Content values for update.
*
* @param compiledStatement the compiled statement
* @return the kripton content values
*/
protected KriptonContentValues contentValuesForUpdate(SupportSQLiteStatement compiledStatement) {
return context.contentValuesForUpdate(compiledStatement);
}
/**
* Creates the helper.
*/
protected void createHelper() {
if (KriptonLibrary.getContext() == null)
throw new KriptonRuntimeException(
"Kripton library is not properly initialized. Please use KriptonLibrary.init(context) somewhere at application startup");
if (this.logEnabled) {
if (options.inMemory) {
Logger.info("In-memory database");
} else {
File dbFile = KriptonLibrary.getContext().getDatabasePath(name);
Logger.info("Database file %s", dbFile.getAbsolutePath());
}
}
Builder config = SupportSQLiteOpenHelper.Configuration.builder(KriptonLibrary.getContext()).name(name)
.callback(new SupportSQLiteOpenHelper.Callback(version) {
@Override
public void onConfigure(@NonNull SupportSQLiteDatabase db) {
AbstractDataSource.this.onConfigure(db);
}
@Override
public void onCorruption(@NonNull SupportSQLiteDatabase db) {
AbstractDataSource.this.onCorruption(db);
}
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
sqliteHelper.setWriteAheadLoggingEnabled(true);
AbstractDataSource.this.onCreate(db);
}
@Override
public void onDowngrade(@NonNull SupportSQLiteDatabase db, int oldVersion, int newVersion) {
AbstractDataSource.this.onDowngrade(db, oldVersion, newVersion);
}
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
sqliteHelper.setWriteAheadLoggingEnabled(true);
AbstractDataSource.this.onOpen(db);
}
@Override
public void onUpgrade(@NonNull SupportSQLiteDatabase db, int oldVersion, int newVersion) {
AbstractDataSource.this.onUpgrade(db, oldVersion, newVersion);
}
});
sqliteHelper = options.openHelperFactory.create(config.build());
if (this.logEnabled) {
Logger.debug("Database helper factory class is %s", options.openHelperFactory.getClass().getName());
Logger.debug("Database helper class is %s", sqliteHelper.getClass().getName());
}
}
private void deleteDatabaseFile(String fileName) {
if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) {
return;
}
if (this.logEnabled) {
Logger.fatal("deleting the database file: " + fileName);
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
SQLiteDatabase.deleteDatabase(new File(fileName));
} else {
try {
final boolean deleted = new File(fileName).delete();
if (!deleted && this.logEnabled) {
Logger.fatal("Could not delete the database file " + fileName);
}
} catch (Exception error) {
if (this.logEnabled) {
Logger.fatal("error while deleting corrupted database file " + error.getMessage());
}
}
}
} catch (Exception e) {
if (this.logEnabled) {
/* print warning and ignore exception */
Logger.warn("delete failed: ", e);
}
}
}
protected void endLock() {
lockDb.unlock();
}
/**
* Force close. It's a dangerous command. It closes the database.
*/
public void forceClose() {
openCounter.set(0);
if (!this.options.inMemory) {
// Closing database
if (database != null) {
clearCompiledStatements();
sqliteHelper.close();
}
database = null;
}
if (logEnabled)
Logger.info("database %s IS FORCED TO BE CLOSED (%s) (connections: %s)", name, status.get(), openCounter.intValue());
}
/**
*
* Return database object or runtimeexception if no database is opened.
*
*
* @return the SQ lite database
*/
public SupportSQLiteDatabase getDatabase() {
if (database == null)
throw (new KriptonRuntimeException(
"No database connection is opened before use " + this.getClass().getCanonicalName()));
return database;
}
public String getName() {
return name;
}
/**
* Get error listener, in transations.
*
* @return the on error listener
*/
public OnErrorListener getOnErrorListener() {
return onErrorListener;
}
/**
* Gets the version.
*
* @return the version
*/
public int getVersion() {
return version;
}
/**
*
* True if dataSource is just created
*
* .
*
* @return true, if is just created
*/
public boolean isJustCreated() {
return justCreated;
}
/**
* Checks if is log enabled.
*
* @return true, if is log enabled
*/
public boolean isLogEnabled() {
return context.isLogEnabled();
}
/**
*
* return true if database is already opened.
*
*
* @return true if database is opened, otherwise false
*/
public boolean isOpen() {
return database != null && database.isOpen() && database.isDbLockedByCurrentThread();
}
/**
* Return true
if any operation is running on datasource,
* false
if database is currently closed.
*
* @return
*/
public boolean isAnyPendingOperation() {
return openCounter.get() > 0;
}
/**
*
* return true if database is already opened in write mode.
*
*
* @return true if database is opened, otherwise false
*/
public boolean isOpenInWriteMode() {
return database != null && database.isOpen() && !database.isReadOnly() && database.isDbLockedByCurrentThread();
}
/**
* Checks if is upgraded version.
*
* @return the upgradedVersion
*/
public boolean isUpgradedVersion() {
return versionChanged;
}
/**
*
*/
private void manageStatus() {
switch (status.get()) {
case READ_AND_WRITE_OPENED:
if (database == null)
status.set(TypeStatus.CLOSED);
unlockReadWriteAccess();
break;
case READ_ONLY_OPENED:
if (database == null)
status.set(TypeStatus.CLOSED);
unlockReadAccess();
break;
case CLOSED:
// do nothing
break;
}
}
private void unlockReadAccess() {
if (!options.neverClose) {
lockReadAccess.unlock();
}
}
private void unlockReadWriteAccess() {
if (!options.neverClose) {
lockReadWriteAccess.unlock();
}
}
/**
* Returns true
if the database need foreign keys
*
* @return
*/
public abstract boolean hasForeignKeys();
/**
* On configure.
*
* @param database the database
*/
protected void onConfigure(SupportSQLiteDatabase database) {
// configure database
if (options.databaseLifecycleHandler != null) {
options.databaseLifecycleHandler.onConfigure(database);
}
}
/**
* The method invoked when database corruption is detected. Default
* implementation will delete the database file.
*
* @param db the {@link SupportSQLiteDatabase} object representing the
* database on which corruption is detected.
*/
protected void onCorruption(@NonNull SupportSQLiteDatabase db) {
// the following implementation is taken from {@link
// DefaultDatabaseErrorHandler}.
if (this.logEnabled) {
Logger.fatal("Corruption reported by sqlite on database: " + db.getPath());
}
try {
if (options.databaseLifecycleHandler != null) {
options.databaseLifecycleHandler.onCorruption(db);
}
} catch (Throwable e) {
e.printStackTrace();
}
// is the corruption detected even before database could be 'opened'?
if (!db.isOpen()) {
// database files are not even openable. delete this database file.
// NOTE if the database has attached databases, then any of them
// could be corrupt.
// and not deleting all of them could cause corrupted database file
// to remain and
// make the application crash on database open operation. To avoid
// this problem,
// the application should provide its own {@link
// DatabaseErrorHandler} impl class
// to delete ALL files of the database (including the attached
// databases).
deleteDatabaseFile(db.getPath());
return;
}
List> attachedDbs = null;
try {
// Close the database, which will cause subsequent operations to
// fail.
// before that, get the attached database list first.
try {
attachedDbs = db.getAttachedDbs();
} catch (SQLiteException e) {
/* ignore */
}
try {
db.close();
} catch (IOException e) {
/* ignore */
}
} finally {
// Delete all files of this corrupt database and/or attached
// databases
if (attachedDbs != null) {
for (android.util.Pair p : attachedDbs) {
deleteDatabaseFile(p.second);
}
} else {
// attachedDbs = null is possible when the database is so
// corrupt that even
// "PRAGMA database_list;" also fails. delete the main database
// file
deleteDatabaseFile(db.getPath());
}
}
}
/**
* On create.
*
* @param database the database
*/
protected abstract void onCreate(SupportSQLiteDatabase database);
/**
* On downgrade.
*
* @param db the db
* @param oldVersion the old version
* @param newVersion the new version
*/
protected void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
if (options.databaseLifecycleHandler != null) {
options.databaseLifecycleHandler.onUpdate(db, oldVersion, newVersion, false);
versionChanged = true;
}
}
protected void onOpen(SupportSQLiteDatabase db) {
if (AbstractDataSource.this.options.databaseLifecycleHandler != null) {
AbstractDataSource.this.options.databaseLifecycleHandler.onOpen(db);
versionChanged = true;
}
}
/**
* On session closed.
*
* @return the sets the
*/
protected Set onSessionClosed() {
return this.context.onSessionClosed();
}
/**
* On session opened.
*/
protected void onSessionOpened() {
this.context.onSessionOpened();
}
/**
* On upgrade.
*
* @param db the db
* @param oldVersion the old version
* @param newVersion the new version
*/
protected void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
if (AbstractDataSource.this.options.databaseLifecycleHandler != null) {
AbstractDataSource.this.options.databaseLifecycleHandler.onUpdate(db, oldVersion, newVersion, true);
versionChanged = true;
}
}
/**
* Open a database, if it is needed in
*
* @param writeMode
* @return
*/
protected Pair openDatabaseThreadSafeMode(boolean writeMode) {
Pair result = new Pair<>();
try {
// lock entire operation set
beginLock();
boolean needToOpened = writeMode ? !this.isOpenInWriteMode() : !this.isOpen();
result.value0 = needToOpened;
// in this part we can not lock lockReadWriteAccess, otherwise it
// may be a
// blocking race
// we lock lockReadWriteAccess after we release
if (needToOpened) {
if (writeMode || options.neverClose) {
result.value1 = openWritableDatabase(false);
} else {
result.value1 = openReadOnlyDatabase(false);
}
} else {
result.value1 = this.getDatabase();
}
} finally {
// unlock entire operation set
endLock();
if (!options.neverClose) {
if (writeMode) {
lockWriteAccess();
} else {
lockReadAccess();
}
}
}
return result;
}
private void lockReadAccess() {
if (!options.neverClose) {
lockReadAccess.lock();
}
}
private void lockWriteAccess() {
if (!options.neverClose) {
lockReadWriteAccess.lock();
}
}
public SupportSQLiteDatabase openReadOnlyDatabase() {
if (!this.options.neverClose) {
return openReadOnlyDatabase(true);
} else {
return openWritableDatabase(true);
}
}
/**
*
* Open a read only database.
*
*
* @return read only database
*/
protected SupportSQLiteDatabase openReadOnlyDatabase(boolean lock) {
if (lock) {
// if I lock this in dbLock. the last one remains locked too
lockReadAccess();
beginLock();
}
try {
if (sqliteHelper == null)
createHelper();
status.set(TypeStatus.READ_ONLY_OPENED);
if (openCounter.incrementAndGet() == 1) {
// open new read database
if (database == null) {
sqliteHelper.setWriteAheadLoggingEnabled(true);
database = sqliteHelper.getReadableDatabase();
database.setForeignKeyConstraintsEnabled(hasForeignKeys());
}
if (logEnabled)
Logger.info("database %s OPENED %s (connections: %s)", getDataSourceSafeName(), status.get(), (openCounter.intValue() - 1));
} else {
if (logEnabled)
Logger.info("database %s VIRTUALLY OPEN %s (connections: %s)", getDataSourceSafeName(), status.get(), (openCounter.intValue() - 1));
}
} catch (Throwable e) {
if (logEnabled) {
Logger.fatal("database %s error during open operation: %s", getDataSourceSafeName(), e.getMessage());
e.printStackTrace();
}
throw (e);
} finally {
if (lock)
endLock();
}
return database;
}
/**
*
* open a writable database.
*
*
* @return writable database
*/
public SupportSQLiteDatabase openWritableDatabase() {
return openWritableDatabase(true);
}
protected SupportSQLiteDatabase openWritableDatabase(boolean lock) {
if (lock) {
// if we open in this thread,
if (!options.neverClose) {
lockWriteAccess();
}
// if I lock this in dbLock the last one remains locked too
beginLock();
}
try {
if (sqliteHelper == null)
createHelper();
status.set(TypeStatus.READ_AND_WRITE_OPENED);
if (openCounter.incrementAndGet() == 1) {
if (logEnabled)
Logger.info("database %s %sOPENED %s (connections: %s)", getDataSourceSafeName(), options.neverClose && database != null ? "VIRTUALLY " : "", status.get(), (openCounter.intValue() - 1));
// open new write database
if (database == null) {
sqliteHelper.setWriteAheadLoggingEnabled(true);
database = sqliteHelper.getWritableDatabase();
database.setForeignKeyConstraintsEnabled(hasForeignKeys());
}
} else {
if (logEnabled)
Logger.info("database %s VIRTUALLY OPENED %s (connections: %s)", getDataSourceSafeName(), status.get(), (openCounter.intValue() - 1));
}
} catch (Throwable e) {
if (logEnabled) {
Logger.fatal("database %s error during open operation: %s", getDataSourceSafeName(), e.getMessage());
e.printStackTrace();
}
throw (e);
} finally {
if (lock)
endLock();
}
return database;
}
/**
* Set error listener for transactions.
*
* @param onErrorListener the new on error listener
*/
public void setOnErrorListener(OnErrorListener onErrorListener) {
this.onErrorListener = onErrorListener;
}
/**
* Sql builder.
*
* @return the string builder
*/
protected StringBuilder sqlBuilder() {
return context.sqlBuilder();
}
}