package co.smartreceipts.android.persistence.database.tables; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteOpenHelper; import android.support.annotation.NonNull; import com.google.common.base.Preconditions; import com.hadisatrio.optional.Optional; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import co.smartreceipts.android.model.Trip; import co.smartreceipts.android.persistence.database.operations.DatabaseOperationMetadata; import co.smartreceipts.android.persistence.database.tables.adapters.SelectionBackedDatabaseAdapter; import co.smartreceipts.android.persistence.database.tables.keys.PrimaryKey; import co.smartreceipts.android.persistence.database.tables.ordering.OrderBy; import co.smartreceipts.android.sync.model.Syncable; import co.smartreceipts.android.sync.provider.SyncProvider; import co.smartreceipts.android.utils.log.Logger; import io.reactivex.Single; /** * Extends the {@link AbstractColumnTable} class to provide support for an extra method, {@link #get(Trip)}. We may * want to generify this class further (to support other classes beside just {@link Trip} objects in the future), but * it'll stay hard-typed for now until this requirement arises... * * @param <ModelType> the model object that CRUD operations here should return * @param <PrimaryKeyType> the primary key type (e.g. Integer, String) that will be used */ public abstract class TripForeignKeyAbstractSqlTable<ModelType, PrimaryKeyType> extends AbstractSqlTable<ModelType, PrimaryKeyType> { private final HashMap<Trip, List<ModelType>> mPerTripCache = new HashMap<>(); private final SelectionBackedDatabaseAdapter<ModelType, PrimaryKey<ModelType, PrimaryKeyType>, Trip> mSelectionBackedDatabaseAdapter; private final String mTripForeignKeyReferenceColumnName; private final String mSortingOrderColumn; public TripForeignKeyAbstractSqlTable(@NonNull SQLiteOpenHelper sqLiteOpenHelper, @NonNull String tableName, @NonNull SelectionBackedDatabaseAdapter<ModelType, PrimaryKey<ModelType, PrimaryKeyType>, Trip> databaseAdapter, @NonNull PrimaryKey<ModelType, PrimaryKeyType> primaryKey, @NonNull String tripForeignKeyReferenceColumnName, @NonNull String sortingOrderColumn) { super(sqLiteOpenHelper, tableName, databaseAdapter, primaryKey, new OrderBy(sortingOrderColumn, true)); mSelectionBackedDatabaseAdapter = databaseAdapter; mTripForeignKeyReferenceColumnName = Preconditions.checkNotNull(tripForeignKeyReferenceColumnName); mSortingOrderColumn = Preconditions.checkNotNull(sortingOrderColumn); } /** * Fetches all model objects with a foreign key reference to the parameter object * * @param trip the {@link Trip} parameter that should be treated as a foreign key * @return a {@link Single} with: all objects assigned to this foreign key in descending order */ @NonNull public Single<List<ModelType>> get(@NonNull Trip trip) { return get(trip, true); } /** * Fetches all model objects with a foreign key reference to the parameter object * * @param trip the {@link Trip} parameter that should be treated as a foreign key * @param isDescending {@code true} for descending order, {@code false} for ascending * @return a {@link Single} with: all objects assigned to this foreign key in the desired order */ @NonNull public synchronized Single<List<ModelType>> get(@NonNull final Trip trip, final boolean isDescending) { return Single.fromCallable(() -> TripForeignKeyAbstractSqlTable.this.getBlocking(trip, isDescending)); } @NonNull public synchronized List<ModelType> getBlocking(@NonNull Trip trip, boolean isDescending) { // We only cache descending entries final boolean cacheResults = isDescending; if (mPerTripCache.containsKey(trip) && cacheResults) { return new ArrayList<>(mPerTripCache.get(trip)); } Cursor cursor = null; try { final List<ModelType> results = new ArrayList<>(); cursor = getReadableDatabase().query(getTableName(), null, mTripForeignKeyReferenceColumnName + "= ? AND " + COLUMN_DRIVE_MARKED_FOR_DELETION + " = ?", new String[]{ trip.getName(), Integer.toString(0) }, null, null, new OrderBy(mSortingOrderColumn, isDescending).getOrderByPredicate()); if (cursor != null && cursor.moveToFirst()) { do { results.add(mSelectionBackedDatabaseAdapter.readForSelection(cursor, trip, isDescending)); } while (cursor.moveToNext()); } if (cacheResults) { mPerTripCache.put(trip, results); } return new ArrayList<>(results); } finally { if (cursor != null) { cursor.close(); } } } @NonNull @Override public synchronized List<ModelType> getBlocking() { final List<ModelType> results = super.getBlocking(); final HashMap<Trip, List<ModelType>> localCache = new HashMap<>(); for (int i = 0; i < results.size(); i++) { final ModelType modelType = results.get(i); final Trip trip = getTripFor(modelType); if (!mPerTripCache.containsKey(trip)) { // Note: we only populate items here that haven't been previously added to the cache if (localCache.containsKey(trip)) { final List<ModelType> perTripResults = localCache.get(trip); perTripResults.add(modelType); } else { localCache.put(trip, new ArrayList<>(Collections.singletonList(modelType))); } } } mPerTripCache.putAll(localCache); return results; } @SuppressWarnings("unchecked") @Override public synchronized Optional<ModelType> insertBlocking(@NonNull ModelType modelType, @NonNull DatabaseOperationMetadata databaseOperationMetadata) { final Optional<ModelType> insertedItem = super.insertBlocking(modelType, databaseOperationMetadata); if (insertedItem.isPresent()) { final Trip trip = getTripFor(insertedItem.get()); if (mPerTripCache.containsKey(trip)) { final List<ModelType> perTripResults = mPerTripCache.get(trip); perTripResults.add(insertedItem.get()); if (insertedItem.get() instanceof Comparable<?>) { Collections.sort((List<? extends Comparable>)perTripResults); } } } return insertedItem; } @SuppressWarnings("unchecked") @Override public synchronized Optional<ModelType> updateBlocking(@NonNull ModelType oldModelType, @NonNull ModelType newModelType, @NonNull DatabaseOperationMetadata databaseOperationMetadata) { final Optional<ModelType> updatedItem = super.updateBlocking(oldModelType, newModelType, databaseOperationMetadata); if (updatedItem.isPresent()) { Logger.debug(this, "Successfully updated this item in our table"); final Trip oldTrip = getTripFor(oldModelType); if (mPerTripCache.containsKey(oldTrip)) { final List<ModelType> perTripResults = mPerTripCache.get(oldTrip); perTripResults.remove(oldModelType); Logger.debug(this, "Found this item in our cache. Removing it"); } boolean isMarkedForDeletion = false; if (updatedItem.get() instanceof Syncable) { final Syncable syncable = (Syncable) newModelType; if (syncable.getSyncState().isMarkedForDeletion(SyncProvider.GoogleDrive)) { isMarkedForDeletion = true; } } final Trip newTrip = getTripFor(updatedItem.get()); if (!isMarkedForDeletion && mPerTripCache.containsKey(newTrip)) { Logger.debug(this, "This item is not marked for deletion. Adding it to our cache"); final List<ModelType> perTripResults = mPerTripCache.get(newTrip); perTripResults.add(updatedItem.get()); if (updatedItem.get() instanceof Comparable<?>) { Collections.sort((List<? extends Comparable>)perTripResults); } } } return updatedItem; } public synchronized void updateParentBlocking(@NonNull Trip oldTrip, @NonNull Trip newTrip) { final ContentValues contentValues = new ContentValues(); contentValues.put(mTripForeignKeyReferenceColumnName, newTrip.getName()); getWritableDatabase().update(getTableName(), contentValues, mTripForeignKeyReferenceColumnName + "= ?", new String[]{ oldTrip.getName() }); mPerTripCache.remove(oldTrip); } @Override public synchronized Optional<ModelType> deleteBlocking(@NonNull ModelType modelType, @NonNull DatabaseOperationMetadata databaseOperationMetadata) { final Optional<ModelType> deleteResult = super.deleteBlocking(modelType, databaseOperationMetadata); if (deleteResult.isPresent()) { final Trip trip = getTripFor(modelType); if (mPerTripCache.containsKey(trip)) { final List<ModelType> perTripResults = mPerTripCache.get(trip); perTripResults.remove(modelType); } } return deleteResult; } public synchronized void deleteParentBlocking(@NonNull Trip trip) { getWritableDatabase().delete(getTableName(), mTripForeignKeyReferenceColumnName + "= ?", new String[]{ trip.getName() }); mPerTripCache.remove(trip); } @Override public synchronized boolean deleteSyncDataBlocking(@NonNull SyncProvider syncProvider) { final boolean success = super.deleteSyncDataBlocking(syncProvider); if (success) { // Clear out our cached data, so we're not out of sync mPerTripCache.clear(); } return success; } @Override public synchronized void clearCache() { super.clearCache(); mPerTripCache.clear(); } /** * Gets the parent {@link Trip} for this {@link ModelType} instance * * @param modelType the {@link ModelType} to get the trip for * @return the parent {@link Trip} instance */ @NonNull protected abstract Trip getTripFor(@NonNull ModelType modelType); }