SQLiteHouse.java
package net.zer0bandwidth.android.lib.database.sqlitehouse;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import android.util.Log;
import net.zer0bandwidth.android.lib.database.SQLitePortal;
import net.zer0bandwidth.android.lib.database.querybuilder.DeletionBuilder;
import net.zer0bandwidth.android.lib.database.querybuilder.QueryBuilder;
import net.zer0bandwidth.android.lib.database.querybuilder.SelectionBuilder;
import net.zer0bandwidth.android.lib.database.querybuilder.UpdateBuilder;
import net.zer0bandwidth.android.lib.database.sqlitehouse.annotations.SQLiteColumn;
import net.zer0bandwidth.android.lib.database.sqlitehouse.annotations.SQLiteDatabaseSpec;
import net.zer0bandwidth.android.lib.database.sqlitehouse.annotations.SQLiteTable;
import net.zer0bandwidth.android.lib.database.sqlitehouse.exceptions.IntrospectionException;
import net.zer0bandwidth.android.lib.database.sqlitehouse.exceptions.SchematicException;
import net.zer0bandwidth.android.lib.database.sqlitehouse.refractor.NullRefractor;
import net.zer0bandwidth.android.lib.database.sqlitehouse.refractor.Refractor;
import net.zer0bandwidth.android.lib.database.sqlitehouse.refractor.RefractorMap;
import net.zer0bandwidth.android.lib.util.LexicalStringComparator;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* Uses custom annotations to automatically construct and manage SQLite
* databases with tables in which each row holds a serialization of a specified
* Java class.
*
* <p>This class is based on {@link SQLitePortal} and provides all of the same
* methods for accessing the database.</p>
*
* <h2>Usage</h2>
*
* <h3>Defining a Database</h3>
*
* <p>Define your SQLite database by creating an empty descendant of
* {@code SQLiteHouse}, decorated with an {@link SQLiteDatabaseSpec} annotation
* which describes the basic parameters of the database itself. In the example
* below, we create a database to hold information about people, places, and
* things.</p>
*
* <pre>
* {@literal @}SQLiteDatabaseSpec(
* database_name = "my_database",
* schema_version = 1,
* classes = { Person.class, Place.class, Thing.class }
* )
* public class MyDatabaseClass
* extends SQLiteHouse<MyDatabaseClass>
* {}
* </pre>
*
* <p>Note that the descendant class extends {@code SQLiteHouse} with a generic
* parameter pointing back to itself. This template parameter is used in
* {@code SQLiteHouse}'s method definitions to ensure that all methods that are
* "fluid" (<i>i.e.</i>, which return the same object) will return instances of
* that descendant class, rather than being typecast up the hierarchy to
* {@code SQLiteHouse} itself. This pattern allows for more effective method
* chaining in case the descendant has custom methods that are also fluid.</p>
*
* <h3>Defining the Database Schema</h3>
*
* <p>In the example above, the {@code classes} element of the annotation names
* three other classes. These are the data objects that you would use in your
* app to contain the data elements that are stored in the database. By
* decorating these classes with annotations, the {@code SQLiteHouse} can
* recognize those classes as data schema definitions, and use the annotations
* to construct and manage the database automatically.</p>
*
* <p>To continue the previous example, the {@code Person} class is shown below;
* {@code Place} and {@code Thing} would be similarly defined.</p>
*
* <pre>
* {@literal @}SQLiteTable( "people" )
* public class Person implements SQLightable
* {
* {@literal @}SQLiteColumn( name = "person_id", index = 0 )
* {@literal @}SQLitePrimaryKey
* protected String m_sID ;
*
* {@literal @}SQLiteColumn( name = "first_name", index = 1 )
* protected String m_sFirstName ;
*
* {@literal @}SQLiteColumn( name = "last_name", index = 2 )
* protected String m_sLastName ;
*
* {@literal @}SQLiteColumn( name = "birthday", index = 3 )
* protected Calendar m_dBirthdate ;
*
* {@literal @}SQLiteColumn( name = "address", index = 4 )
* protected String m_sAddress ;
*
* /** Schema classes must provide a default constructor. {@literal *}/
* public Person() {}
*
* // other constructors, methods, etc. follow
* }
* </pre>
*
* <p>Note that the instance members that hold the data need not have the same
* name as their database columns, nor are they forced to be {@code public}.
* This system of annotations is designed such that it interferes as little as
* possible with the other design decisions that might go into the data classes.
* As long as the fields that correspond to database table columns are properly
* decorated, they will be discovered and used in the database. Note also that
* this allows the data object to have any other member fields it wants, which
* are <i>not</i> serialized into the database, merely by leaving those members
* undecorated.</p>
*
* <p>The {@code @SQLitePrimaryKey} annotation explicitly designates a data
* element which could be used as a primary key for the table. However, the
* {@code SQLiteHouse} will not actually define the column as such; it will
* merely be {@code UNIQUE NOT NULL} in the table creation SQL, and a standard,
* magic {@code _id} column will be used as the actual primary key. This is done
* because of SQLite's inherent preference for auto-incremented integer keys.
* However, the {@code SQLiteHouse} will behave as if this object field is the
* actual primary key, allowing consumers to search tables by this field rather
* than the magic numeric ID.</p>
*
* <p>For notes on the predictability of column order in the table definition,
* see the {@link SQLightable.Reflection.ColumnSequencer} class.</p>
*
* <h3>Constructing a Database Instance</h3>
*
* <p>Use the {@link SQLiteHouse.Factory} class to construct an instance of the
* database class. The factory will perform all the necessary pre-processing of
* the {@link SQLiteDatabaseSpec} annotation and feed those parameters into the
* constructor for the database class instance. The factory is templatized such
* that your database class does not need to extend it.</p>
*
* <p>The code below continues our example of the {@code MyDatabaseClass} by
* constructing an instance.</p>
*
* <pre>
* // given some Context ctx in which the class will operate
* // given some SQLiteHouse.CursorFactory cf for the database helper
*
* MyDatabaseClass dbh = SQLiteHouse.Factory.init().getInstance(
* MyDatabaseClass.class, ctx, cf ) ;
* </pre>
*
* <h3>Custom Processors for Data Classes</h3>
*
* <p>{@code SQLiteHouse} uses implementations of the {@link Refractor}
* interface to process various data types. The standard set of implementations,
* generally named "lenses", are automatically constructed and mapped by the
* {@link RefractorMap} class.</p>
*
* <p>To customize this mapping with your own {@code Refractor} implementations,
* you may explicitly name a refractor class in the {@link SQLiteColumn}
* annotation for any individual field in a schematic class. This is the most
* efficient way to define a custom refractor, as it will be picked up
* automatically by {@code SQLiteHouse} during the introspection process.</p>
*
* <h3>Connecting to the Database</h3>
*
* <p>Since this class extends {@link SQLitePortal}, which in turn is descended
* from {@link android.database.sqlite.SQLiteOpenHelper}, it provides the same
* methods for managing connections to the database. Connections may be
* established with {@link SQLitePortal#openDB()} and released with
* {@link SQLitePortal#close()}.</p>
*
* <p>The {@link #onCreate} and {@link #onUpgrade} methods, which ensure that
* the underlying database is always installed with the current schema, are
* already implemented in {@code SQLiteHouse}, and use the schematic information
* discovered by the constructor to handle the database creation and upgrade
* operations automatically. Descendant classes need not provide their own
* implementations of these methods, unless they require some exotic
* post-processing logic after the normal creation/update process has been
* completed.</p>
*
* <h3>Using the Query Commands</h3>
*
* <p>The base {@code SQLiteHouse} class provides implementations of some basic
* query operations — insertion, selection, updates, and deletion. Once
* all of the schematic data is known to the instance, the grammar of
* interacting with the database flows relatively simply.</p>
*
* <pre>
* MyDatabaseClass dbh = SQLiteHouse.Factory.init().getInstance(
* MyDatabaseClass.class, ctx, null ) ;
* dbh.openDB() ;
* // wait for connection, either by sleeping or catching connection event
*
* Person alice = new Person( UUID.randomUUID().toString(),
* "Alice", "Appleton", new GregorianCalendar( 1980, 6, 3 ),
* "1687 Newton Way, Principia, NY 10705" ) ;
* dbh.insert( alice ) ;
*
* Person bob = new Person( UUID.randomUUID().toString(),
* "Bob", "Bullhead", new GregorianCalendar( 1960, 9, 5 ),
* "230 South Bouquet St., Oakland, PA 15213" ) ;
* long idBob = dbh.insert( bob ) ;
*
* bob.setAddress( "115 Federal Street, Pittsburgh, PA 15212" ) ;
* int nUpdated = dbh.update( bob ) ; // nUpdated == 1
* Person also_bob = dbh.select( idBob ) ;
* String sBobAddress = also_bob.getAddress() ; // 115 Federal Street...
*
* int nDeleted = dbh.delete( alice ) ; // nDeleted == 1
* nDeleted = dbh.delete( alice ) ; // nDeleted == 0
* nDeleted = dbh.delete( bob ) ; // nDeleted == 1
* nDeleted = dbh.delete( also_bob ) ; // nDeleted == 0
*
* dbh.close() ;
* </pre>
*
* <p>The descendant class may, as with {@link SQLitePortal}, define further
* custom instance methods to perform more specific reusable queries.</p>
*
* @param <DSC> A descendant class. When creating a descendant class, it should
* extend {@code SQLiteHouse} templatized for itself. This will ensure that all
* methods inherited from {@code SQLiteHouse} return instances of the
* descendant class, rather than the parent class.
*
* @since zer0bandwidth-net/android 0.1.4 (#26)
*/
public class SQLiteHouse<DSC extends SQLiteHouse>
extends SQLitePortal
{
/// Static Inner Classes ///////////////////////////////////////////////////////
/**
* Creates instances of a {@link SQLiteHouse}. Because we have to have all
* our ducks in a row before the {@code SQLiteHouse} calls its ancestor's
* constructor, the factory needs to parse the {@link SQLiteDatabaseSpec} in
* advance, and feed that information into the constructor as parameters.
*
* <h3>Usage</h3>
*
* <p>Given a {@code Context ctx} and {@code SQLiteDatabase.CursorFactory cf}
* (which may be null):</p>
*
* <pre>
* MyDatabaseClass dbh = SQLiteHouse.Factory.init().getInstance(
* MyDatabaseClass.class, ctx, cf ) ;
* </pre>
*
* <p>See {@link SQLiteHouse} for detailed information regarding how this
* fits into the overall lifecycle of the object instance.</p>
*
* @since zer0bandwidth-net/android 0.1.4 (#26)
*/
public static class Factory
{
/**
* Simply an alias for the default constructor, to allow for a cleaner
* grammar when creating a factory instance to obtain a reference to a
* database.
* @return an instance of the factory
*/
public static SQLiteHouse.Factory init()
{ return new SQLiteHouse.Factory() ; }
/** The context in which the {@code SQLiteHouse} will operate. */
protected Context m_ctx = null ;
/**
* The name of the database to be created. The factory will glean this
* from the {@link SQLiteDatabaseSpec} annotation of the class that is
* passed into the {@link #getInstance} method.
*
* The {@link SQLiteHouse#SQLiteHouse(Factory)} constructor retrieves
* this value from the factory to pass it to the parent class's
* constructor, {@link SQLitePortal#SQLitePortal}.
*/
protected String m_sDatabaseName = null ;
/**
* The cursor factory to be used with the {@link SQLiteHouse} instance,
* if any. This is passed as an argument to {@link #getInstance}.
*
* The {@link SQLiteHouse#SQLiteHouse(Factory)} constructor retrieves
* this value from the factory to pass it to the parent class's
* constructor, {@link SQLitePortal#SQLitePortal}.
*/
protected SQLiteDatabase.CursorFactory m_cf = null ;
/**
* The current schema version of the database. The factory will glean
* this from the {@link SQLiteDatabaseSpec} annotation of the class that
* is passed into the {@link #getInstance} method.
*
* The {@link SQLiteHouse#SQLiteHouse(Factory)} constructor retrieves
* this value from the factory to pass it to the parent class's
* constructor, {@link SQLitePortal#SQLitePortal}.
*/
protected int m_nSchemaVersion = SCHEMA_NOT_DEFINED ;
/**
* The array of classes which, in aggregate, define the schema for the
* database. The factory will glean this list from the
* {@link SQLiteDatabaseSpec} annotation of the class that is passed to
* the {@link #getInstance} method.
*
* The {@link SQLiteHouse#SQLiteHouse(Factory)} constructor uses this
* list to construct the schema; see
* {@link SQLiteHouse#setSchemaClasses(List)}.
*/
protected ArrayList<Class<? extends SQLightable>> m_aclsSchema = null ;
/**
* Uses annotations found in a {@link SQLiteHouse} descendant to
* construct an instance of the database class.
*
* Since this method is templatized on the class that is being
* instantiated, there is no need for the {@code SQLiteHouse} descendant
* to provide its own extension of {@code SQLiteHouse.Factory}; this
* method will return an instance of the descendant class.
* @param cls the {@code SQLiteHouse} descendant class being created
* @param ctx the context in which the object will operate
* @param <FDSC> the {@code SQLiteHouse} descendant being created; this
* matches the class sent in the {@code cls} argument.
* @return an instance of the {@code SQLiteHouse} descendant,
* initialized with the database attributes found in the class's
* {@link SQLiteDatabaseSpec} annotation
* @throws IntrospectionException if something goes wrong while
* processing the descendant class. When invoking the descendant's
* constructor, there are several possible failure states; use
* {@code .getCause()} to determine which one applies.
* @since zer0bandwidth-net/android 0.1.7 (#50)
*/
public <FDSC extends SQLiteHouse> FDSC getInstance(
Class<FDSC> cls, Context ctx )
throws IntrospectionException
{ return this.getInstance( cls, ctx, null ) ; }
/**
* Uses annotations found in a {@link SQLiteHouse} descendant to
* construct an instance of the database class.
*
* Since this method is templatized on the class that is being
* instantiated, there is no need for the {@code SQLiteHouse} descendant
* to provide its own extension of {@code SQLiteHouse.Factory}; this
* method will return an instance of the descendant class.
* @param cls the {@code SQLiteHouse} descendant class being created
* @param ctx the context in which the object will operate
* @param cf a cursor factory as allowed by the
* {@link android.database.sqlite.SQLiteOpenHelper} constructor (may be
* null)
* @param <FDSC> the {@code SQLiteHouse} descendant being created; this
* matches the class sent in the {@code cls} argument.
* @return an instance of the {@code SQLiteHouse} descendant,
* initialized with the database attributes found in the class's
* {@link SQLiteDatabaseSpec} annotation
* @throws IntrospectionException if something goes wrong while
* processing the descendant class. When invoking the descendant's
* constructor, there are several possible failure states; use
* {@code .getCause()} to determine which one applies.
*/
@SuppressWarnings( "unchecked" ) // Constructor is invoked from class.
public <FDSC extends SQLiteHouse> FDSC getInstance( Class<FDSC> cls,
Context ctx, SQLiteDatabase.CursorFactory cf )
throws IntrospectionException
{
this.m_ctx = ctx ;
this.m_cf = cf ;
try
{
SQLiteDatabaseSpec spec =
cls.getAnnotation( SQLiteDatabaseSpec.class ) ;
m_sDatabaseName = spec.database_name() ;
m_nSchemaVersion = spec.schema_version() ;
this.m_aclsSchema = new ArrayList<>() ;
this.m_aclsSchema.addAll( Arrays.asList( spec.classes() ) ) ;
}
catch( NullPointerException x )
{
throw new IntrospectionException(
"Could not initialize the database instance." ) ;
}
try
{
Constructor ctor = cls.getDeclaredConstructor(
SQLiteHouse.Factory.class ) ;
ctor.setAccessible( true ) ;
return ((FDSC)( ctor.newInstance( this ) )) ;
}
catch( Exception x )
{
// Might catch any of:
// IllegalAccessException, InstantiationException,
// InvocationTargetException, NoSuchMethodException
throw new IntrospectionException(
"Could not find appropriate constructor in descendant.",
x ) ;
}
}
}
/**
* Used by {@link SQLiteHouse} to sort the indices and/or names of columns
* within a table specification.
* @since zer0bandwidth-net/android 0.1.4 (#26)
* @deprecated zer0bandwidth-net/android 0.2.1 (#56) — replaced by
* {@link SQLightable.Reflection.ColumnSequencer}
*/
public static class ColumnIndexComparator
implements Comparator<Field>
{
/**
* The algorithm in this method prefers to sort a column with an
* explicit index definition before any column with no index definition.
* For any pair of columns that have the same defined index, or where
* neither column has a defined index, the algorithm will sort columns
* alphabetically by name instead. The only way to have this method
* return {@code 0} (equal) would be to have two columns with the same
* name, which is a violation of SQL table requirements anyway.
* @param fldFirst the first column to be compared
* @param fldSecond the second column to be compared
* @return {@code -1} if the first column should be before the second;
* {@code 1} if the first column should be after the second; {@code 0}
* if no sort criteria can be resolved.
*/
@Override
public int compare( Field fldFirst, Field fldSecond )
{
SQLiteColumn antFirst =
fldFirst.getAnnotation( SQLiteColumn.class ) ;
SQLiteColumn antSecond =
fldSecond.getAnnotation( SQLiteColumn.class ) ;
// Try comparing the "index" attribute first.
if( antFirst.index() == SQLiteColumn.NO_INDEX_DEFINED )
{
if( antSecond.index() != SQLiteColumn.NO_INDEX_DEFINED )
{ // Always sort cols without indices after cols with indices.
return 1 ;
}
}
else if( antSecond.index() == SQLiteColumn.NO_INDEX_DEFINED )
{ // Always sort cols without indices after cols with indices.
return -1 ;
}
else if( antFirst.index() < antSecond.index() )
return -1 ;
else if( antFirst.index() > antSecond.index() )
return 1 ;
// If "index" is equal, the sort alphabetically.
return (new LexicalStringComparator()).compare(
antFirst.name(), antSecond.name() ) ;
}
}
/**
* A short-lived, open data structure which provides context for various
* operations within the class. Because so many of the values fetched here
* must be reused multiple times within the body of certain larger
* functions, or must be passed <i>between</i> functions, it is useful to
* have all of these fields gathered in a single contextual container.
* Since 0.1.7 (#50), this class is not particularly different from
* {@link SQLightable.Reflection}.
* @since zer0bandwidth-net/android 0.1.4 (#26)
* @deprecated zer0bandwidth-net/android 0.2.1 (#56) — use
* {@link SQLightable.Reflection} instead, by using
* {@link #getReflection(Class)} to fetch the one that the database class
* has already generated, or by using
* {@link SQLightable.Reflection#reflect(Class)} to generate a new one.
*/
@SuppressWarnings("deprecation") // TODO (deprecation) remove in future major version
public static class QueryContext<DBH extends SQLiteHouse>
{
/** A persistent reference back to a database portal. */
public DBH house = null ;
/** The schematic class providing this context. */
public Class<? extends SQLightable> clsTable = null ;
/** The schematic class's table-defining annotation. */
public SQLiteTable antTable = null ;
/** The name of the table. */
public String sTableName = null ;
/** The field of the schematic class in this context, if any. */
public Field fldColumn = null ;
/** The context field's column-defining annotation. */
public SQLiteColumn antColumn = null ;
/** The name of the column. */
public String sColumnName = null ;
/** Indicates whether the column is annotated as a key. */
public boolean bColumnIsKey = false ;
/** The refractor appropriate for this column type. */
public Refractor lens = null ;
/** The value of this column in some instance, if set. */
public String sColumnSQLValue = null ;
/**
* Constructs the instance and binds it back to a {@link SQLiteHouse}.
* @param dbh the helper instance
* @see SQLiteHouse#getQueryContext()
*/
public QueryContext( DBH dbh )
{ this.house = dbh ; }
/**
* Loads contextual information pertaining to the table defined by the
* specified schematic class. This operation clears all
* previously-loaded table data, and any data that might have been
* loaded for a column of that table.
* @param cls the schematic class
* @param <SC> the schematic class
* @return (fluid)
* @throws SchematicException if the class isn't part of the schema
*/
public <SC extends SQLightable> QueryContext<DBH> loadTableDef( Class<SC> cls )
throws SchematicException
{
this.clsTable = cls ;
//noinspection unchecked - guaranteed logically
SQLightable.Reflection<SC> tbl = this.house.getReflection(cls) ;
this.antTable = tbl.getTableAttrs() ;
this.sTableName = tbl.getTableName() ;
this.clearColumnDef() ;
return this ;
}
/**
* Loads contextual information pertaining to a column of the table
* already set by {@link #loadTableDef}. This operation will clear the
* value of any previously-analyzed column.
* @param fld the field to be set for context
* @return (fluid)
*/
public QueryContext<DBH> loadColumnDef( Field fld )
{
if( fld == null )
return this.clearColumnDef() ;
this.fldColumn = fld ;
SQLightable.Reflection.Column col = this.house
.m_mapReflections.get(this.clsTable).getColumnDef(fld) ;
this.antColumn = col.getColAttrs() ;
this.sColumnName = this.antColumn.name() ;
this.bColumnIsKey = col.isKey() ;
this.lens = col.getRefractor() ;
this.sColumnSQLValue = null ;
return this ;
}
/**
* Loads contextual information pertaining to a column of the table set
* by {@link #loadTableDef}, by searching for that column by its name.
* This operation will clear the value of any previously-analyzed
* column.
* @param sSoughtName the name of the column to be set for context
* @return (fluid)
*/
public QueryContext<DBH> loadColumnDef( String sSoughtName )
{
if( this.clsTable == null )
throw new IllegalStateException( "No table loaded." ) ;
if( TextUtils.isEmpty(sSoughtName) )
return this.clearColumnDef() ;
SQLightable.Reflection tbl =
this.house.getReflection( this.clsTable ) ;
SQLightable.Reflection.Column col = tbl.getColumn( sSoughtName ) ;
if( col != null ) return this.loadColumnDef( col.getField() ) ;
Log.w( LOG_TAG, (new StringBuilder())
.append( "No column found with name [" )
.append( sSoughtName )
.append( "] in table [" )
.append( this.sTableName )
.append( "] defined by class [" )
.append( this.clsTable.getCanonicalName() )
.append( "]; CLEARING loaded column data." )
.toString()
);
return this.clearColumnDef() ;
}
/**
* Clears any and all contextual data pertaining to a table column.
* @return (fluid)
*/
protected QueryContext<DBH> clearColumnDef()
{
this.fldColumn = null ;
this.antColumn = null ;
this.sColumnName = null ;
this.bColumnIsKey = false ;
this.lens = null ;
this.sColumnSQLValue = null ;
return this ;
}
/**
* If the context is bound to a specific column, then this method will
* try to discover the value of the field corresponding to that column
* in the specified object instance.
* @param o the schematic object that contains the column field
* @param <SC> the schematic class
* @return (fluid)
* @throws IllegalStateException if inadequate context has been loaded
* @throws NullPointerException if the schematic object is null
* @throws SchematicException if something goes wrong while setting the
* value
*/
public <SC extends SQLightable> QueryContext<DBH> loadColumnValue( SC o )
throws IllegalStateException, NullPointerException, SchematicException
{
this.sColumnSQLValue = null ;
if( o == null )
{
throw new NullPointerException(
"Cannot examine null instance." ) ;
}
if( this.fldColumn == null )
{
throw new IllegalStateException(
"Cannot discover value if no column has been chosen." ) ;
}
if( this.lens == null )
{ throw SchematicException.noLensForField( this.fldColumn, null ); }
try
{
//noinspection unchecked
this.sColumnSQLValue = this.lens.toSQLiteString(
this.lens.getValueFrom( o, this.fldColumn ) ) ;
}
catch( IllegalAccessException xAccess )
{
throw SchematicException.fieldWasInaccessible(
this.clsTable.getSimpleName(),
this.fldColumn.getName(),
xAccess
);
}
return this ;
}
}
/// Static Methods /////////////////////////////////////////////////////////////
// getTableName(Class,SQLiteTable) was removed in 0.2.0 (#49).
/// Static Constants ///////////////////////////////////////////////////////////
/** The tag used by logging statements in this class. */
public static final String LOG_TAG = SQLiteHouse.class.getSimpleName() ;
/**
* Magic constant to indicate that the schema version has not yet been
* resolved.
* @see Factory#m_nSchemaVersion
*/
protected static final int SCHEMA_NOT_DEFINED = -1 ;
/**
* This magic column name is used in every table to auto-create a row ID as
* preferred by SQLite. Table classes should not define a member with this
* name.
*/
public static final String MAGIC_ID_COLUMN_NAME = "_id" ;
/// Instance Members ///////////////////////////////////////////////////////////
/**
* A list of classes that, in aggregate, define the schema for the database.
* @see #setSchemaClasses(List)
*/
protected List<Class<? extends SQLightable>> m_aclsSchema = null ;
/**
* A map of schematic classes to their reflections.
* @since zer0bandwidth-net/android 0.1.7 (#50)
*/
protected SQLightable.ReflectionMap m_mapReflections = null ;
/**
* A persistent instance of a refractor map. Descendant classes may be
* registered for certain data classes if desired.
*/
protected RefractorMap m_mapRefractor = null ;
/// Constructors and Initializers //////////////////////////////////////////////
/**
* Constructor used by the {@link SQLiteHouse.Factory} to create an instance
* of the class. The factory passes itself into this constructor, so that it
* can provide values for all of the parameters necessary to invoke the
* superclass's constructor.
*
* <p>Descendant classes <b>must</b> extend this constructor in order to use
* the {@link SQLiteHouse.Factory} to properly process the schematic data in
* the various data classes.</p>
*
* <pre>
* protected MyDatabaseClass( SQLiteHouse.Factory factory )
* { super(factory) ; }
* </pre>
*
* @param factory the factory which has resolved information about the
* database to be bound to this class
*/
protected SQLiteHouse( SQLiteHouse.Factory factory )
{
super( factory.m_ctx, factory.m_sDatabaseName,
factory.m_cf, factory.m_nSchemaVersion ) ;
this.setSchemaClasses( factory.m_aclsSchema )
.processReflections()
;
m_mapRefractor = (new RefractorMap()).init() ;
//noinspection deprecation - TODO (deprecation) remove in next major revision
this.registerCustomRefractors() ;
}
/**
* Caches a list of classes that define the database schema.
*
* Consumed by {@link #SQLiteHouse(Factory)}.
*
* @param aclsSchema the list of classes
* @return (fluid)
*/
protected DSC setSchemaClasses( List<Class<? extends SQLightable>> aclsSchema )
{
if( this.m_aclsSchema == null )
this.m_aclsSchema = new ArrayList<>() ;
else
this.m_aclsSchema.clear() ;
this.m_aclsSchema.addAll( aclsSchema ) ;
//noinspection unchecked
return (DSC)this ;
}
/**
* Given that the list of schematic classes has been populated, discover and
* cache their characteristics for future reference.
*
* Consumed by {@link #SQLiteHouse(Factory)}; must follow
* {@link #setSchemaClasses}.
*
* @return (fluid)
* @since zer0bandwidth-net/android 0.1.7 (#50)
*/
protected DSC processReflections()
{
if( m_mapReflections == null )
m_mapReflections = new SQLightable.ReflectionMap() ;
else m_mapReflections.clear() ;
for( Class<? extends SQLightable> cls : m_aclsSchema )
{
SQLightable.Reflection<? extends SQLightable> tbl =
SQLightable.Reflection.reflect(cls) ;
m_mapReflections.put( cls, tbl ) ;
}
//noinspection unchecked
return (DSC)this ;
}
/**
* Consumed by the constructor, this method registers any custom
* {@link Refractor} implementations that should be used by the instance.
* The default implementation of this method returns trivially; descendants
* of {@code SQLiteHouse} may override this method to add any custom
* {@code Refractor} implementations here.
* @return (fluid)
* @see Refractor
* @see RefractorMap
* @deprecated zer0bandwidth-net/android 0.2.1 (#56) — instead, use
* the {@code refractor} attribute of the {@link SQLiteColumn} annotation
* to define refractors for each relevant field in your schematic classes
*/
@SuppressWarnings( { "unchecked", "DeprecatedIsStillUsed" } )
protected DSC registerCustomRefractors()
{ return (DSC)this ; } // trivially
/// net.zer0bandwidth.android.lib.database.SQLitePortal ////////////////////////
/**
* Called by Android when the consumer tries to connect to the database.
* This method will iterate over the list of table classes and execute the
* SQL statement which will create that table.
*
* <p>This method was designed to be a {@code final} implementation, but is
* left extensible for descendant classes, just in case they might need to
* perform any custom post-processing.</p>
*
* @param db a direct handle to the SQLite database (provided by the Android
* OS)
* @throws SchematicException <i>(since 0.1.7 #50)</i> if something goes
* wrong while processing the schema definition
*/
@Override
public void onCreate( SQLiteDatabase db )
throws SchematicException
{
Log.i( LOG_TAG, "Executing onCreate()" ) ;
for( Class<? extends SQLightable> clsTable : m_aclsSchema )
{
SQLightable.Reflection tbl = this.getReflection(clsTable) ;
String sSQL = null ;
try
{
sSQL = tbl.getTableCreationSQL() ;
db.execSQL( sSQL ) ;
}
catch( Exception x )
{
throw SchematicException
.tableCreationOrUpdateFailed( tbl, sSQL, x ) ;
}
}
}
/**
* Called by Android when the consumer tries to connect to the database, and
* the current schema version in the class is newer than the one that is
* currently installed. This method iterates over the list of schema classes
* and, if the table's {@code since} version is newer than the old version,
* will create the table. Otherwise, it will analyze the table's columns,
* and if any column's {@code since} version is newer than the old version,
* the method will add the column to the table.
*
* <p>This method was designed to be a {@code final} implementation, but is
* left extensible for descendant classes, just in case they might need to
* perform any custom post-processing.</p>
*
* @param db a direct handle to the SQLite database (provided by the Android
* OS)
* @param nOld the version of the schema that is installed
* @param nNew the version of the schema that is defined
*/
@Override
public void onUpgrade( SQLiteDatabase db, int nOld, int nNew )
{
Log.i( LOG_TAG, (new StringBuilder())
.append( "Executing onUpgrade() from old version [" )
.append( nOld ).append( "] to new version [" )
.append( nNew ).append( "]..." )
.toString()
);
for( Class<? extends SQLightable> clsTable : m_aclsSchema )
this.upgradeTable( db, clsTable, nOld ) ;
}
/**
* Called by {@link #onUpgrade} to upgrade a single SQLite database table
* based on the information reflected by a specific schematic class.
* @param db a direct handle to the SQLite database (provided to
* {@link #onUpgrade} by the Android OS)
* @param clsTable the schematic class that defines the table
* @param nOld the version of the schema that is installed
* @param <SC> the schematic class
* @return (fluid)
* @throws SchematicException if the class is not part of the schema
* @since zer0bandwidth-net/android 0.1.7 (#50)
*/
protected <SC extends SQLightable> SQLiteHouse<DSC> upgradeTable(
SQLiteDatabase db, Class<SC> clsTable, int nOld )
throws SchematicException
{
SQLightable.Reflection<SC> tbl = this.getReflection(clsTable) ;
int nTableSince = tbl.getFirstSchemaVersion() ;
if( nTableSince > nOld )
{ // The whole table is new; create it.
db.execSQL( tbl.getTableCreationSQL() ) ;
Log.i( LOG_TAG, (new StringBuilder())
.append( "Created table [" ).append( tbl.getTableName() )
.append( "] from class [" )
.append( clsTable.getCanonicalName() )
.append( "]." )
.toString()
);
}
else
{ // Examine the table's columns, creating new ones where needed.
// SQLightable.Reflection.ColumnMap<SC> mapColumns = tbl.getColumnMap() ;
// for( SQLightable.Reflection<SC>.Column col : mapColumns.values() )
for( SQLightable.Reflection<SC>.Column col : tbl.getColumns() )
{
int nColSince = col.getColAttrs().since() ;
if( nColSince > nOld )
{
db.execSQL( tbl.getAddColumnSQL(col) ) ;
Log.i( LOG_TAG, (new StringBuilder())
.append( "Added column [" )
.append( col.getName() )
.append( "] to table [" )
.append( tbl.getTableName() )
.append( "] from class [" )
.append( clsTable.getCanonicalName() )
.append( "]." )
.toString()
);
}
}
}
return this ;
}
/// Query Commands /////////////////////////////////////////////////////////////
/**
* Inserts an object of a known schematic class into the database.
*
* Since v0.1.5 (#43), the method also tries to write the auto-incremented
* row ID back into the object instance, if that class has a field annotated
* to contain that column.
*
* @param o the object to be inserted
* @return the row ID of the inserted record
*/
public <ROW extends SQLightable> long insert( ROW o )
throws SchematicException
{
//noinspection unchecked
Class<ROW> cls = ((Class<ROW>)( o.getClass() ));
SQLightable.Reflection<ROW> tbl = this.getReflection(cls) ;
SQLightable.Reflection<ROW>.Column col = tbl.getMagicIDColumn() ;
ContentValues vals = tbl.toContentValues(o) ;
// Don't allow the passed object to dictate the auto-inc ID.
if( col != null ) vals.remove( MAGIC_ID_COLUMN_NAME ) ;
long nID = QueryBuilder
.insertInto( m_db, tbl.getTableName() )
.setValues( vals )
.execute()
;
if( col != null )
{ // Try to write the ID back into the instance.
try { col.getField().setLong( o, nID ) ; }
catch( IllegalAccessException xAccess )
{ Log.w( LOG_TAG, "Couldn't rewrite row ID into object." ) ; }
}
return nID ;
}
/**
* Updates the values of an object from a known schematic class.
* @param o the object to be updated
* @return the number of rows updated (generally 1)
* @throws SchematicException if the data object's class isn't part of the
* schema, or if the table definition for this class didn't specify its own
* primary key
*/
public <ROW extends SQLightable> int update( ROW o )
throws SchematicException
{
//noinspection unchecked
SQLightable.Reflection<ROW> tbl = ((SQLightable.Reflection<ROW>)
( this.getReflection( o.getClass() ) )) ;
SQLightable.Reflection<ROW>.Column col = tbl.getKeyOrMagicIDColumn() ;
if( col == null )
{
throw new SchematicException(
"Can't use update(SQLightable) without a key column." ) ;
}
return QueryBuilder.update( m_db, tbl.getTableName() )
.setValues( tbl.toContentValues(o) )
.where( String.format( "%s=%s",
col.getName(), col.getSQLColumnValueFrom(o) ) )
.execute()
;
}
/**
* Shorthand to obtain an {@link UpdateBuilder} bound to this database and
* targeting the table corresponding to the specified schematic class.
* @param cls the class that defines part of the schema
* @return an {@code UPDATE} query builder prepared for that table
* @throws SchematicException if the class is not part of the schema
*/
public <SC extends SQLightable> UpdateBuilder update( Class<SC> cls )
throws SchematicException
{
return QueryBuilder.update( m_db,
this.getReflection(cls).getTableName() ) ;
}
/**
* Searches the database for a row of the table represented by the supplied
* object, such that the primary key value in that object equals the primary
* key found in the object. The method does not alter the supplied object;
* instead, it returns a new instance with the values found in the database.
* @param oCriteria the object whose primary key will be used as the
* criteria for a search
* @param <ROW> the specific {@link SQLightable} implementation being sought
* @return a new instance of the schematic class, populated with values from
* a row of the database
* @throws SchematicException if anything goes wrong along the way
*/
public <ROW extends SQLightable> ROW search( ROW oCriteria )
throws SchematicException
{
//noinspection unchecked
SQLightable.Reflection<ROW> tbl = ((SQLightable.Reflection<ROW>)
( this.getReflection( oCriteria.getClass() ) )) ;
SQLightable.Reflection<ROW>.Column col = tbl.getKeyOrMagicIDColumn() ;
if( col == null )
{
throw new SchematicException(
"Can't use search(SQLightable) without a key column." ) ;
}
Cursor crs = null ;
try
{
crs = QueryBuilder.selectFrom( m_db, tbl.getTableName() )
.where( String.format( "%s=%s",
col.getName(), col.getSQLColumnValueFrom(oCriteria) ))
.execute()
;
if( ! crs.moveToFirst() ) return null ; // No such object found.
return tbl.fromCursor(crs) ;
}
finally
{ closeCursor(crs) ; }
}
/**
* Searches the database for a row of the table represented by the supplied
* schematic class, such that the primary key column value matches the value
* supplied in the method call.
* @param cls the schematic class being sought
* @param sID the unique identifier of the row, which <i>must</i> be a
* string in this flavor of the method
* @param <ROW> the schematic class being sought
* @return a new instance of the schematic class, populated with values from
* a row of the database
* @throws SchematicException if anything goes wrong along the way
* @since zer0bandwidth-net/android 0.1.5
*/
public <ROW extends SQLightable> ROW search( Class<ROW> cls, String sID )
throws SchematicException
{
SQLightable.Reflection<ROW> tbl = this.getReflection(cls) ;
SQLightable.Reflection<ROW>.Column colKey = tbl.getKeyColumn() ;
if( colKey == null )
{
throw new SchematicException(
"Can't use search(Class,String) without a key column." ) ;
}
Cursor crs = null ;
try
{
crs = QueryBuilder.selectFrom( m_db, tbl.getTableName() )
.where( String.format( "%s='%s'",
colKey.getName(), sID ) )
.execute()
;
if( ! crs.moveToFirst() ) return null ; // No such object found.
return tbl.fromCursor(crs) ;
}
finally
{ closeCursor(crs) ; }
}
/**
* Searches the database for a row of the table represented by the supplied
* object, where the specified integer is equal to the row's magic auto-ID.
* @param cls the schematic class that will contain the row
* @param nID the auto-incremented integer ID of the row
* @param <ROW> the schematic class
* @return a new instance of the schematic class, containing the row with
* the specified auto-ID
* @throws SchematicException if the class is not part of the schema
*/
@SuppressLint("DefaultLocale")
public <ROW extends SQLightable> ROW select( Class<ROW> cls, long nID )
throws SchematicException
{
SQLightable.Reflection<ROW> tbl = this.getReflection(cls) ;
Cursor crs = null ;
try
{
crs = QueryBuilder.selectFrom( m_db, tbl.getTableName() )
.where( String.format( "%s=%d",
MAGIC_ID_COLUMN_NAME, nID ) )
.execute()
;
if( ! crs.moveToFirst() ) return null ; // No such object found.
return tbl.fromCursor(crs) ;
}
finally
{ closeCursor(crs) ; }
}
/**
* Shorthand to obtain a {@link SelectionBuilder} bound to this database and
* targeting the table corresponding to the specified schematic class.
* @param cls the schematic class
* @param <SC> the schematic class
* @return a {@code SELECT} query builder prepared for that table
* @throws SchematicException if the class is not part of the schema
*/
public <SC extends SQLightable> SelectionBuilder selectFrom( Class<SC> cls )
throws SchematicException
{
return QueryBuilder.selectFrom( m_db,
this.getReflection(cls).getTableName() ) ;
}
/**
* Searches the database for a row of the table represented by the supplied
* objects, and deletes that row.
* @param o the schematic class instance to be deleted if found
* @param <ROW> the schematic class
* @return the number of rows deleted
* @throws SchematicException if the table is not part of the schema, or
* doesn't specify a key column
*/
public <ROW extends SQLightable> int delete( ROW o )
throws SchematicException
{
//noinspection unchecked
SQLightable.Reflection<ROW> tbl = ((SQLightable.Reflection<ROW>)
( this.getReflection( o.getClass() ) )) ;
SQLightable.Reflection<ROW>.Column col = tbl.getKeyOrMagicIDColumn() ;
if( col == null )
{
throw new SchematicException(
"Can't use delete(SQLightable) without a key column." ) ;
}
return QueryBuilder.deleteFrom( m_db, tbl.getTableName() )
.where( String.format( "%s=%s",
col.getName(), col.getSQLColumnValueFrom(o) ) )
.execute()
;
}
/**
* Shorthand to obtain a {@link DeletionBuilder} bound to this database and
* targeting the table corresponding to the specified schematic class.
* @param cls the class that defines part of the schema
* @return a {@code DELETE} query builder prepared for that table
* @throws SchematicException if the class is not part of the schema
*/
public <SC extends SQLightable> DeletionBuilder deleteFrom( Class<SC> cls )
throws SchematicException
{
return QueryBuilder.deleteFrom( m_db,
this.getReflection(cls).getTableName() ) ;
}
/// Other Instance Methods /////////////////////////////////////////////////////
/**
* Accesses the reflection of a class that is marshalled by this instance.
* If the class was not included in the list of classes that is part of the
* instance's schema, then the method returns {@code null}, rather than
* invoking {@link SQLightable.Reflection#reflect(Class)}.
* @param cls the schematic class to be described
* @param <SC> the schematic class to be described
* @return the reflection of that class as stored in this instance
* @since zer0bandwidth-net/android 0.1.7 (#50)
*/
public <SC extends SQLightable> SQLightable.Reflection<SC> describe( Class<SC> cls )
{ return m_mapReflections.get(cls) ; }
/**
* Similar to {@link #describe}, but will throw a {@link SchematicException}
* if the specified class is not found. This is more useful internally than
* as a generaly utility, hence the {@code protected} access control.
* @param cls the schematic class to be described
* @param <SC> the schematic class to be described
* @return the reflection of that class as stored in this instance
* @throws SchematicException if the class is not found in this instance
* @since zer0bandwidth-net/android 0.1.7 (#50)
*/
protected <SC extends SQLightable> SQLightable.Reflection<SC> getReflection( Class<SC> cls )
throws SchematicException
{
SQLightable.Reflection<SC> tbl = m_mapReflections.get(cls) ;
if( tbl == null )
throw SchematicException.classNotInSchema( cls, m_aclsSchema ) ;
return tbl ;
}
/**
* Reads a row of data from a cursor, and marshals it into a schematic class
* instance corresponding to the table from which the row was fetched.
* @param crs the cursor from which data will be marshalled
* @param cls the schematic class to which data will be marshalled
* @param <SC> the schematic class to which data will be marshalled
* @return an instance of the class, containing the cursor's current row
* @throws SchematicException if the class is not part of the schema
*/
public <SC extends SQLightable> SC fromCursor( Cursor crs, Class<SC> cls )
throws SchematicException
{ return this.getReflection(cls).fromCursor(crs) ; }
/**
* Given a result set loaded into a {@link Cursor}, iterate over that cursor
* to produce a list of schematic class instances containing the rows in the
* result set.
* @param cls the schematic class which could contain each row
* @param crs the cursor containing the result set
* @param <SC> the schematic class which could contain each row
* @return a list of schematic class instances, containing the rows of the
* result set
* @throws SchematicException if any instance cannot be instantiated
* @since zer0bandwidth-net/android 0.1.5 (#43)
*/
public <SC extends SQLightable> List<SC> processResultSet(
Class<SC> cls, Cursor crs )
throws SchematicException
{
List<SC> aResults = new ArrayList<>() ;
if( crs.moveToFirst() )
{ // Process each element in turn, marshalling it into the list.
do aResults.add( this.getReflection(cls).fromCursor(crs) ) ;
while( crs.moveToNext() ) ;
}
return aResults ;
}
/**
* Creates an empty query context bound to this database helper.
* @return a context object
* @deprecated zer0bandwidth-net/android 0.2.1 (#56)
*/ // TODO (deprecation) remove in next major revision
@SuppressWarnings( { "unchecked", "deprecation" } )
public QueryContext<DSC> getQueryContext()
{ return new QueryContext<>( (DSC)this ) ; }
/**
* Creates a query context bound to this database helper, and pre-loads the
* information for a specified table.
* @param clsTable the schematic table to be pre-loaded
* @return a context object
* @deprecated zer0bandwidth-net/android 0.2.1 (#56) — use
* {@link #getReflection(Class)} instead
*/ // TODO (deprecation) remove in next major revision
@SuppressWarnings( "deprecation" )
public QueryContext<DSC> getQueryContext( Class<? extends SQLightable> clsTable )
{ return this.getQueryContext().loadTableDef(clsTable) ; }
/**
* Accesses the cache of schematic classes for the database.
* @return the list of schematic classes loaded into this instance
* @since zer0bandwidth-net/android 0.1.7 (#50)
*/
public List<Class<? extends SQLightable>> getSchemaClasses()
{ return m_aclsSchema ; }
/**
* Discovers the type of refractor needed to marshal the specified field.
*
* Since 0.1.5 (#41), the method will try to discover whether there is a
* usable custom implementation specified in the column annotation, and
* return that if such a specification exists.
*
* @param fld a field in a schematic class
* @return the refractor which would marshal that class
* @throws IntrospectionException if no refractor can be discovered
*/
public Refractor<?> getRefractorForField( Field fld )
throws IntrospectionException
{
SQLiteColumn antCol = fld.getAnnotation( SQLiteColumn.class ) ;
Class<? extends Refractor> clsLens = antCol.refractor() ;
if( clsLens != NullRefractor.class ) try
{ // The field explicitly defines a custom refractor. Use it.
return clsLens.newInstance() ;
}
catch( Exception x )
{
Log.w( LOG_TAG, (new StringBuilder())
.append( "Cannot instantiate custom refractor class [" )
.append( clsLens.getCanonicalName() )
.append( "]." )
.toString(),
x ) ;
}
try { return m_mapRefractor.get( fld.getType() ).newInstance() ; }
catch( Exception x )
{
throw new IntrospectionException( (new StringBuilder())
.append( "Could not instantiate a refractor for field [" )
.append( fld.getName() )
.append( "]:" )
.toString(),
x ) ;
}
}
}