package com.jdroid.android.sqlite.repository; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import com.jdroid.android.application.AbstractApplication; import com.jdroid.android.sqlite.Column; import com.jdroid.android.sqlite.SQLiteHelper; import com.jdroid.java.collections.Lists; import com.jdroid.java.domain.Entity; import com.jdroid.java.repository.Repository; import com.jdroid.java.utils.LoggerUtils; import com.jdroid.java.utils.StringUtils; import org.slf4j.Logger; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * Repository implementation which uses SQLite. * * @param <T> Entity type. */ public abstract class SQLiteRepository<T extends Entity> implements Repository<T> { private static final Logger LOGGER = LoggerUtils.getLogger(SQLiteRepository.class); protected SQLiteHelper dbHelper; private EntityChildrenListener<T> entityChildrenListener; /** * Constructor. It register create SQL statements in {@link SQLiteHelper}. * * @param dbHelper {@link SQLiteHelper} to be used be repository. */ public SQLiteRepository(SQLiteHelper dbHelper, EntityChildrenListener<T> entityChildrenListener) { this.dbHelper = dbHelper; this.dbHelper.addCreateSQL(getCreateTableSQL()); this.entityChildrenListener = entityChildrenListener; } public SQLiteRepository(SQLiteHelper dbHelper) { this(dbHelper, null); } /** * Returns the name of the table which store the entities. * * @return the table name. */ protected abstract String getTableName(); /** * Returns the column definitions for the table. * * @return columns. */ protected abstract Column[] getColumns(); /** * Returns the name of the column used as id By default it returns {@link Column#ID}. * * @return the id column name. */ protected String getIdColumnName() { return Column.ID; } /** * Returns the name of the column used as parent id. By default it returns {@link Column#PARENT_ID}. * * @return the parent id column name. */ protected String getParentIdColumnName() { return Column.PARENT_ID; } /** * Creates and populate an entity instance which data from cursor. It does NOT populate entity children. * * @param cursor cursor to get data. * @return an entity instance. */ protected abstract T createObjectFromCursor(Cursor cursor); /** * Creates and populate an instance of {@link ContentValues} which the entity data. * * @param item entity to store. * @return the {@link ContentValues} instance. */ protected abstract ContentValues createContentValuesFromObject(T item); /** * Default sort to be used in ORDER BY section of queries. * * @return default sort. */ protected String getDefaultSort() { return null; } /** * Begins a transaction if there is not a transaction started yet. * * @param db Database. * @return true if a transaction has been started. */ protected boolean beginTransaction(SQLiteDatabase db) { boolean endTransaction = false; if (!db.inTransaction()) { db.beginTransaction(); endTransaction = true; LOGGER.trace("Begin transaction"); } return endTransaction; } /** * Mark current transaction as successful. Only mark the transaction if endTransaction parameter is true. * * @param db Database. * @param endTransaction Indicates if the transaction should be closed for the current operation. */ protected void successTransaction(SQLiteDatabase db, boolean endTransaction) { if (endTransaction) { db.setTransactionSuccessful(); LOGGER.trace("Success transaction"); } } /** * Ends current transaction if endTransaction parameter is true. * * @param db Database. * @param endTransaction Indicates if the transaction should be closed for the current operation. */ protected void endTransaction(SQLiteDatabase db, boolean endTransaction) { if (endTransaction) { db.endTransaction(); LOGGER.trace("End transaction"); } } @Override @SuppressWarnings("resource") public T get(String id) { SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = null; try { cursor = db.query(getTableName(), getProjection(), getIdColumnName() + "=?", new String[] { id }, null, null, null); T item = null; if (cursor.moveToNext()) { item = createObjectFromCursor(cursor); if (entityChildrenListener != null) { entityChildrenListener.onLoaded(item); } } LOGGER.trace("Retrieved object from database of type: " + getTableName() + " id: " + id); return item; } finally { if (cursor != null) { cursor.close(); } } } @SuppressWarnings("resource") @Override public List<T> findByField(String fieldName, Object... values) { SQLiteDatabase db = dbHelper.getReadableDatabase(); String selection; String[] selectionArgs = null; if ((values != null) && (values.length > 0)) { selectionArgs = new String[values.length]; StringBuilder sb = new StringBuilder(fieldName + " IN ("); for (int i = 0; i < values.length; i++) { selectionArgs[i] = values[i].toString(); if (i > 0) { sb.append(","); } sb.append("?"); } sb.append(")"); selection = sb.toString(); } else { selection = fieldName + " IS NULL"; } Cursor cursor = null; try { ArrayList<T> items = new ArrayList<>(); cursor = db.query(getTableName(), getProjection(), selection, selectionArgs, null, null, getDefaultSort()); while (cursor.moveToNext()) { T item = createObjectFromCursor(cursor); if (entityChildrenListener != null) { entityChildrenListener.onLoaded(item); } items.add(item); } cursor.close(); LOGGER.trace("Retrieved objects from database [" + items.size() + "] of type: " + getTableName() + ". Field: " + fieldName + " | Values: " + values); return items; } finally { if (cursor != null) { cursor.close(); } } } @Override public List<T> getAll(List<String> ids) { return findByField(getIdColumnName(), ids); } @Override public List<T> getAll() { return findByField(null, (Object[])null); } @Override public Boolean isEmpty() { return getSize() == 0; } @SuppressWarnings("resource") @Override public Long getSize() { SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = null; try { cursor = db.query(getTableName(), new String[0], null, null, null, null, null); return (long)cursor.getCount(); } finally { if (cursor != null) { cursor.close(); } } } @Override public void add(T item) { @SuppressWarnings("resource") SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean endTransaction = beginTransaction(db); try { ContentValues values = createContentValuesFromObject(item); Long id = db.insertOrThrow(getTableName(), null, values); if (item.getId() == null) { item.setId(id.toString()); } if (entityChildrenListener != null) { entityChildrenListener.onStored(item); } LOGGER.trace("Stored object in database: " + item); successTransaction(db, endTransaction); } finally { endTransaction(db, endTransaction); } } @Override public void addAll(Collection<T> items) { @SuppressWarnings("resource") SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean endTransaction = beginTransaction(db); try { for (T item : items) { add(item); } LOGGER.trace("Stored objects in database:\n" + items); successTransaction(db, endTransaction); } finally { endTransaction(db, endTransaction); } } @Override public void update(T item) { @SuppressWarnings("resource") SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean endTransaction = beginTransaction(db); try { ContentValues values = createContentValuesFromObject(item); db.update(getTableName(), values, getIdColumnName() + "=?", new String[]{item.getId()}); if (entityChildrenListener != null) { entityChildrenListener.onUpdated(item); } LOGGER.trace("Updated object in database: " + item); successTransaction(db, endTransaction); } finally { endTransaction(db, endTransaction); } } @Override public void replaceAll(Collection<T> items) { @SuppressWarnings("resource") SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean endTransaction = beginTransaction(db); try { List<String> idsToKeep = Lists.newArrayList(); for (T item : items) { idsToKeep.add(item.getId()); } db.delete(getTableName(), getIdColumnName() + " NOT IN (" + StringUtils.join(idsToKeep) + ")", null); LOGGER.trace("Deleted from database all objects of type: " + getTableName() + " with ids != to " + idsToKeep); for (T item : items) { add(item); } successTransaction(db, endTransaction); } finally { endTransaction(db, endTransaction); } } @Override public void remove(T item) { remove(item.getId()); } @Override public void remove(String id) { @SuppressWarnings("resource") SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean endTransaction = beginTransaction(db); try { T item = null; if (entityChildrenListener != null) { item = get(id); } db.delete(getTableName(), getIdColumnName() + "=?", new String[] { id }); if (entityChildrenListener != null) { entityChildrenListener.onRemoved(item); } LOGGER.trace("Deleted object in database with id: " + id); successTransaction(db, endTransaction); } finally { endTransaction(db, endTransaction); } } @Override public void removeAll() { @SuppressWarnings("resource") SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean endTransaction = beginTransaction(db); try { List<T> all = null; if (entityChildrenListener != null) { all = getAll(); } db.delete(getTableName(), null, null); if (entityChildrenListener != null) { for (T item : all) { entityChildrenListener.onRemoved(item); } } LOGGER.trace("Deleted from database all objects of type: " + getTableName()); successTransaction(db, endTransaction); } finally { endTransaction(db, endTransaction); } } @Override public void removeAll(Collection<T> items) { @SuppressWarnings("resource") SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean endTransaction = beginTransaction(db); try { for (T item : items) { remove(item); } LOGGER.trace("Deleted objects in database: " + items); successTransaction(db, endTransaction); } finally { endTransaction(db, endTransaction); } } @Override public T getUniqueInstance() { List<T> all = getAll(); if (all.isEmpty()) { return null; } LOGGER.trace("Retrieved single instance of type: " + getTableName()); return all.get(0); } /** * This method allows to replace all entity children of a given parent, it will remove any children which are not in * the list, add the new ones and update which are in the list.. * * @param list of children to replace. * @param parentId id of parent entity. */ public void replaceChildren(List<T> list, String parentId) { for (T item : list) { item.setParentId(parentId); } @SuppressWarnings("resource") SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean endTransaction = beginTransaction(db); try { db.delete(getTableName(), getParentIdColumnName() + "=?", new String[] { parentId }); addAll(list); LOGGER.trace("Replaced children of parent " + parentId + " and type " + getTableName()); successTransaction(db, endTransaction); } finally { endTransaction(db, endTransaction); } } /** * This method allows to replace all entity children of a given parent, it will remove any children which are not in * the list, add the new ones and update which are in the list.. * * @param list of children to replace. * @param parentId id of parent entity. * @param clazz entity class. */ public static <T extends Entity> void replaceChildren(List<T> list, String parentId, Class<T> clazz) { SQLiteRepository<T> repository = (SQLiteRepository<T>)AbstractApplication.get().getRepositoryInstance(clazz); repository.replaceChildren(list, parentId); } /** * Creates the SQL statement to create the table according columns definitions. * * @return SQL statement. */ public String getCreateTableSQL() { StringBuilder builder = new StringBuilder(); // Add columns for (Column column : getColumns()) { addColumn(builder, column); builder.append(", "); } // Add references StringBuilder referencesBuilder = new StringBuilder(); for (Column column : getColumns()) { if (column.getReference() != null) { referencesBuilder.append("FOREIGN KEY(").append(column.getColumnName()).append(") REFERENCES ").append( column.getReference().getTableName()).append("(").append( column.getReference().getColumn().getColumnName()).append(") ON DELETE CASCADE, "); } } // Add unique constraint StringBuilder uniqueBuilder = new StringBuilder(); boolean first = true; for (Column column : getColumns()) { if (column.isUnique()) { if (!first) { uniqueBuilder.append(", "); } first = false; uniqueBuilder.append(column.getColumnName()); } } return getCreateTableSQL(builder.toString(), referencesBuilder.toString(), uniqueBuilder.toString()); } /** * Creates the SQL statement to create the table using given columns definitions. * * @param columns columns definitions. * @param references reference constraints. * @param uniqueColumns unique constraints. * @return SQL statement. */ private String getCreateTableSQL(String columns, String references, String uniqueColumns) { StringBuilder builder = new StringBuilder(); builder.append("CREATE TABLE ").append(getTableName()).append("("); builder.append(columns); builder.append(references); builder.append("UNIQUE (").append(uniqueColumns).append(") ON CONFLICT REPLACE"); builder.append(");"); return builder.toString(); } /** * Generate the SQL definition for a column. * * @param builder current StringBuilder to add the SQL definition. * @param column column definition. */ private void addColumn(StringBuilder builder, Column column) { builder.append(column.getColumnName()); builder.append(" "); builder.append(column.getDataType().getType()); Boolean optional = column.isOptional(); if (optional != null) { builder.append(optional ? " NULL" : " NOT NULL"); } String extraQualifier = column.getExtraQualifier(); if (extraQualifier != null) { builder.append(" "); builder.append(extraQualifier); } } /** * Returns a list of statements to upgrade the SQL scheme. * * @return a list of statements. */ public String[] getDefaultUpgradeSQL() { return new String[] { getCreateTableSQL() }; } /** * Returns the default projection which includes all the columns defined by {@link #getColumns()} * * @return the projection. */ protected String[] getProjection() { String[] projection = new String[getColumns().length]; for (int i = 0; i < projection.length; i++) { projection[i] = getColumns()[i].getColumnName(); } return projection; } public void setEntityChildrenListener(EntityChildrenListener<T> entityChildrenListener) { this.entityChildrenListener = entityChildrenListener; } }