SQLiteHouseRelay.java
package net.zer0bandwidth.android.lib.database.sqlitehouse.content;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import net.zer0bandwidth.android.lib.content.ContentUtils;
import net.zer0bandwidth.android.lib.content.IntentUtils;
import net.zer0bandwidth.android.lib.content.querybuilder.SelectionBuilder;
import net.zer0bandwidth.android.lib.database.sqlitehouse.SQLightable;
import net.zer0bandwidth.android.lib.database.sqlitehouse.SQLiteHouse;
import net.zer0bandwidth.android.lib.database.sqlitehouse.content.exceptions.SQLiteContentException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import static net.zer0bandwidth.android.lib.database.SQLiteSyntax.DELETE_FAILED;
import static net.zer0bandwidth.android.lib.database.SQLiteSyntax.INSERT_FAILED;
import static net.zer0bandwidth.android.lib.database.SQLiteSyntax.UPDATE_FAILED;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.EXTRA_INSERT_ROW_ID;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.EXTRA_RESULT_ROW_COUNT;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.EXTRA_SCHEMA_CLASS_DATA;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.EXTRA_SCHEMA_CLASS_NAME;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.EXTRA_SELECTION_QUERY_SPEC;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.KEEPER_DELETE;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.KEEPER_INSERT;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.KEEPER_SELECT;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.KEEPER_UPDATE;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.RELAY_NOTIFY_DELETE;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.RELAY_NOTIFY_DELETE_FAILED;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.RELAY_NOTIFY_INSERT;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.RELAY_NOTIFY_INSERT_FAILED;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.RELAY_NOTIFY_SELECT_FAILED;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.RELAY_NOTIFY_UPDATE;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.RELAY_NOTIFY_UPDATE_FAILED;
import static net.zer0bandwidth.android.lib.database.sqlitehouse.content.SQLiteHouseSignalAPI.RELAY_RECEIVE_SELECTION;
/**
* A class which can send requests to, and receive notifications from, a
* {@link SQLiteHouseKeeper}.
*
* This class fills the role of a {@link ContentResolver} without implementing
* that class's API, since the prototypes of the {@code ContentResolver}'s
* methods don't fit with the workflow of a {@code SQLiteHouse}.
*
* If an app uses an implementation of {@link SQLiteHouse} to marshal data
* to/from a SQLite database, then it should provide a library which includes
* the schematic classes and an implementation of this class. Since the
* implementation class is expected to be provided in a library that is separate
* from the {@code SQLiteHouse} implementation, its declaration <i>must not</i>
* depend on the ability to import that implementation (<i>e.g.</i> as part of
* a generic template parameter on the class).
*
* @since zer0bandwidth-net/android 0.1.7 (#50)
*/
public class SQLiteHouseRelay
extends BroadcastReceiver
{
/// Inner Classes //////////////////////////////////////////////////////////////
/**
* Methods that must be implemented by any class that wants to process the
* information received in signals from a {@link SQLiteHouseKeeper}.
* @since zer0bandwidth-net/android 0.1.7 (#50)
*/
public interface Listener
{
/**
* Called by {@link SQLiteHouseRelay#onRowInserted} to inform the
* listener of a successful insertion.
* @param nRowID the integer ID of the new database row
*/
void onRowInserted( long nRowID ) ;
/**
* Called by {@link SQLiteHouseRelay#onInsertFailed} to inform the
* listener of a failed insertion.
*/
void onInsertFailed() ;
/**
* Called by {@link SQLiteHouseRelay#onRowsUpdated} to inform the
* listener of a successful update.
* @param nCount the number of rows that were updated
*/
void onRowsUpdated( int nCount ) ;
/**
* Called by {@link SQLiteHouseRelay#onUpdateFailed} to inform the
* listener of a failed update.
*/
void onUpdateFailed() ;
/**
* Called by {@link SQLiteHouseRelay#onRowsDeleted} to inform the
* listener of a successful deletion.
* @param nCount the number of rows that were deleted
*/
void onRowsDeleted( int nCount ) ;
/**
* Called by {@link SQLiteHouseRelay#onDeleteFailed} to inform the
* listener of a failed deletion.
*/
void onDeleteFailed() ;
/**
* Called by {@link SQLiteHouseRelay#onRowsSelected} to pass the results
* of a successful selection to the listener.
* @param cls the schematic class that will marshal the data
* @param nTotalCount the total number of rows
* @param aoRows the rows themselves, already marshalled
* @param <SC> the schematic class
*/
<SC extends SQLightable> void onRowsSelected( Class<SC> cls, int nTotalCount, List<SC> aoRows ) ;
/**
* Called by {@link SQLiteHouseRelay#onSelectFailed} to inform the
* listener of a failed selection.
*/
void onSelectFailed() ;
}
/// Static constants ///////////////////////////////////////////////////////////
public static final String LOG_TAG = SQLiteHouseRelay.class.getSimpleName();
/// Member fields //////////////////////////////////////////////////////////////
/** The context in which the relay will operate. */
protected Context m_ctx = null ;
/** A reference for the contract under which the relay is registered. */
protected SQLiteHouseSignalAPI m_api = null ;
/** The set of active listeners. */
protected Vector<Listener> m_vListeners = null ;
/// Constructors and initializers //////////////////////////////////////////////
/**
* Constructs an instance, but does not register it.
* @param ctx the context in which the relay will operate
*/
public SQLiteHouseRelay( Context ctx )
{
m_ctx = ctx ;
m_vListeners = new Vector<>() ;
}
/**
* Registers the relay instance as a {@link BroadcastReceiver} in its
* context.
* @param api the signal contract between the keeper and the relay; if
* {@code null}, then the relay will be unregistered instead
* @return (fluid)
*/
public SQLiteHouseRelay register( SQLiteHouseSignalAPI api )
{
m_api = api ;
if( api == null )
this.unregister() ;
else
m_ctx.registerReceiver( this, api.getRelayIntentFilter() ) ;
return this ;
}
/**
* Unregisters the relay in its context.
* @return (fluid)
*/
public SQLiteHouseRelay unregister()
{
ContentUtils.unregister( m_ctx, this ) ;
m_api = null ;
return this ;
}
/// android.content.BroadcastReceiver //////////////////////////////////////////
@Override
public final void onReceive( Context ctx, Intent sig )
{
String sAction = IntentUtils.discoverAction(sig) ;
if( sAction == null || sAction.isEmpty() )
{
Log.i( LOG_TAG, "Ignoring request with empty action token." ) ;
return ;
}
if( m_api == null )
{
Log.i( LOG_TAG, (new StringBuilder())
.append( "No signals are registered! Ignoring action [" )
.append( sAction )
.append( "]." )
.toString()
);
return ;
}
String sActionToken = m_api.getTokenFromRelayAction(sAction) ;
switch( sActionToken )
{
case RELAY_NOTIFY_INSERT:
this.onRowInserted( sig ) ;
break ;
case RELAY_NOTIFY_INSERT_FAILED:
this.onInsertFailed( sig ) ;
break ;
case RELAY_NOTIFY_UPDATE:
this.onRowsUpdated( sig ) ;
break ;
case RELAY_NOTIFY_UPDATE_FAILED:
this.onUpdateFailed( sig ) ;
break ;
case RELAY_NOTIFY_DELETE:
this.onRowsDeleted( sig ) ;
break ;
case RELAY_NOTIFY_DELETE_FAILED:
this.onDeleteFailed( sig ) ;
break ;
case RELAY_RECEIVE_SELECTION:
this.onRowsSelected( sig ) ;
break ;
case RELAY_NOTIFY_SELECT_FAILED:
this.onSelectFailed( sig ) ;
break ;
default:
this.handleCustomAction( ctx, sig, sActionToken ) ;
}
}
/// Listener management ////////////////////////////////////////////////////////
/**
* Registers a listener.
* The method is idempotent; if the same listener is passed multiple times,
* then it will be added only if it is not already present.
* @param l the listener to be registered
* @return (fluid)
*/
public SQLiteHouseRelay addListener( Listener l )
{
if( ! m_vListeners.contains(l) )
m_vListeners.add(l) ;
return this ;
}
/**
* Unregisters a listener.
* The method is idempotent; if the same listener is passed multiple times,
* then it will be removed only if it is still present.
* @param l the listener to be removed
* @return (fluid)
*/
public SQLiteHouseRelay removeListener( Listener l )
{
if( m_vListeners.contains(l) )
m_vListeners.remove(l) ;
return this ;
}
/// Action handlers ////////////////////////////////////////////////////////////
/**
* Override this method to handle custom actions not covered by the standard
* set defined in {@link SQLiteHouseSignalAPI}. The default implementation
* writes an information log stating that the action is unrecognized.
* @param ctx the context from which the signal originated
* @param sig the received signal
* @param sToken the action token parsed from the signal
*/
@SuppressWarnings("UnusedParameters") // default intentionally ignores
protected void handleCustomAction( Context ctx, Intent sig, String sToken )
{
Log.i( LOG_TAG, (new StringBuilder())
.append( "Ignoring unrecognized action [" )
.append( sToken )
.append( "]." )
.toString()
);
}
/**
* Handles a signal from the keeper that a row was inserted.
* @param sig the received signal
*/
protected synchronized void onRowInserted( Intent sig )
{
long nRowID = sig.getLongExtra(
m_api.getFormattedExtraTag( EXTRA_INSERT_ROW_ID ),
INSERT_FAILED
);
String sClass = m_api.getExtraSchemaClassName(sig) ;
Log.i( LOG_TAG, (new StringBuilder())
.append( "Row ID [" )
.append(( nRowID == INSERT_FAILED ? "(unknown)" : nRowID ))
.append( "] of class [" )
.append(( sClass == null ? "(unknown)" : sClass ))
.append( "] inserted into the database." )
.toString()
);
for( Listener l : m_vListeners )
l.onRowInserted(nRowID) ;
}
/**
* Handles a signal from the keeper that a row insertion has failed.
* @param sig the received signal
*/
protected synchronized void onInsertFailed( Intent sig )
{
String sClass = m_api.getExtraSchemaClassName(sig) ;
Log.e( LOG_TAG, (new StringBuilder())
.append( "Keeper failed to insert a row of type [" )
.append(( sClass == null ? "(unknown)" : sClass ))
.append( "]." )
.toString()
);
for( Listener l : m_vListeners )
l.onInsertFailed() ;
}
/**
* Handles a signal from the keeper that some rows were updated.
* @param sig the received signal
*/
protected synchronized void onRowsUpdated( Intent sig )
{
int nCount = sig.getIntExtra(
m_api.getFormattedExtraTag( EXTRA_RESULT_ROW_COUNT ),
UPDATE_FAILED
);
String sClass = m_api.getExtraSchemaClassName(sig) ;
Log.i( LOG_TAG, (new StringBuilder())
.append( "Updated [" )
.append(( nCount == UPDATE_FAILED ? "(unknown)" : nCount ))
.append(( nCount == 1 ? "] row of type [" : "] rows of type [" ))
.append(( sClass == null ? "(unknown)" : sClass ))
.append( "]." )
.toString()
);
for( Listener l : m_vListeners )
l.onRowsUpdated(nCount) ;
}
/**
* Handles a signal from the keeper that a table update has failed.
* @param sig the received signal
*/
protected synchronized void onUpdateFailed( Intent sig )
{
String sClass = m_api.getExtraSchemaClassName(sig) ;
Log.e( LOG_TAG, (new StringBuilder())
.append( "Keeper failed to update rows of type [" )
.append(( sClass == null ? "(unknown)" : sClass ))
.append( "]." )
.toString()
);
for( Listener l : m_vListeners )
l.onUpdateFailed() ;
}
/**
* Handles a signal from the keeper that some rows were deleted.
* @param sig the received signal
*/
protected synchronized void onRowsDeleted( Intent sig )
{
int nCount = sig.getIntExtra(
m_api.getFormattedExtraTag( EXTRA_RESULT_ROW_COUNT ),
DELETE_FAILED
);
String sClass = m_api.getExtraSchemaClassName(sig) ;
Log.i( LOG_TAG, (new StringBuilder())
.append( "Deleted [" )
.append(( nCount == DELETE_FAILED ? "(unknown)" : nCount ))
.append(( nCount == 1 ? "] row of type [" : "] rows of type [" ))
.append(( sClass == null ? "(unknown)" : sClass ))
.append( "]." )
.toString()
);
for( Listener l : m_vListeners )
l.onRowsDeleted(nCount) ;
}
/**
* Handles a signal from the keeper that a row deletion failed.
* @param sig the received signal
*/
protected synchronized void onDeleteFailed( Intent sig )
{
String sClass = m_api.getExtraSchemaClassName(sig) ;
Log.e( LOG_TAG, (new StringBuilder())
.append( "Keeper failed to delete rows of type [" )
.append(( sClass == null ? "(unknown)" : sClass ))
.append( "]." )
.toString()
);
for( Listener l : m_vListeners )
l.onDeleteFailed() ;
}
/**
* Handles a signal payload from the keeper, containing a set of selected
* rows from the database.
* @param sig the received signal
* @param <SC> the schematic class
*/
protected synchronized <SC extends SQLightable> void onRowsSelected( Intent sig )
{
int nCount = sig.getIntExtra(
m_api.getFormattedExtraTag( EXTRA_RESULT_ROW_COUNT ), -1 ) ;
if( nCount == -1 )
{ // Short-circuit; signal doesn't tell us how many results there are.
Log.w( LOG_TAG, "No row count included in selection results." ) ;
this.onSelectFailed(sig) ;
}
Class<SC> cls = null ;
try { cls = m_api.getClassFromExtra(sig) ; }
catch( SQLiteContentException x )
{ // Short-circuit; can't figure out how to marshal results.
Log.w( LOG_TAG, (new StringBuilder())
.append( "Can't discover class to marshal [" )
.append( nCount )
.append(( nCount == 1 ? "] result" : "] results" ))
.append( " from the keeper's signal." )
.toString()
, x );
this.onSelectFailed(sig) ;
}
String sExtra = m_api.getFormattedExtraTag( EXTRA_SCHEMA_CLASS_DATA ) ;
if( ! sig.hasExtra(sExtra) )
{ // Short-circuit; can't find the data extra.
Log.w( LOG_TAG, "Selection result signal had no data." ) ;
this.onSelectFailed(sig) ;
}
Parcelable[] apclRows = sig.getParcelableArrayExtra(sExtra) ;
if( apclRows == null )
{
if( nCount > 0 )
Log.w( LOG_TAG, "Could not extract data from signal." ) ;
return ;
}
ArrayList<SC> aoRows = new ArrayList<>( apclRows.length ) ;
SQLightable.Reflection<SC> tbl = m_api.reflect(cls) ;
for( Parcelable pclRow : apclRows )
aoRows.add( tbl.fromBundle( ((Bundle)(pclRow)) ) ) ;
for( Listener l : m_vListeners )
l.onRowsSelected( cls, nCount, aoRows ) ;
}
/**
* Handles a signal from the keeper that a row selection failed.
* @param sig the received signal
*/
protected synchronized void onSelectFailed( Intent sig )
{
String sClass = m_api.getExtraSchemaClassName(sig) ;
Log.e( LOG_TAG, (new StringBuilder())
.append( "Keeper failed to select rows of type [" )
.append(( sClass == null ? "(unknown)" : sClass ))
.append( "]." )
.toString()
);
for( Listener l : m_vListeners )
l.onSelectFailed() ;
}
/// Broadcasts to SQLiteHouseKeeper ////////////////////////////////////////////
/**
* Requests insertion of a schematic class instance into the keeper's
* database.
* @param o an instance of the schematic class
* @param <SC> the schematic class
* @return (fluid)
*/
public <SC extends SQLightable> SQLiteHouseRelay insert( SC o )
{ m_ctx.sendBroadcast( this.buildInsertSignal(o) ) ; return this ; }
/**
* Constructs the {@link Intent} to be sent by {@link #insert}.
* This is a separate method only so that it can be unit-tested.
* @param o an instance of the schematic class to be inserted
* @param <SC> the schematic class
* @return the intent to be sent by {@link @insert}
*/
protected <SC extends SQLightable> Intent buildInsertSignal( SC o )
{
Intent sig = new Intent(
m_api.getFormattedKeeperAction( KEEPER_INSERT ) ) ;
SQLightable.Reflection<SC> tbl = m_api.reflect(o) ;
sig.putExtra( m_api.getFormattedExtraTag( EXTRA_SCHEMA_CLASS_NAME ),
tbl.getTableClass().getCanonicalName() ) ;
sig.putExtra( m_api.getFormattedExtraTag( EXTRA_SCHEMA_CLASS_DATA ),
tbl.toBundle(o) ) ;
return sig ;
}
/**
* Requests an update of a particular row in the keeper's database,
* corresponding to the schematic class instance supplied.
* @param o an instance of the schematic class
* @param <SC> the schematic class
* @return (fluid)
*/
public <SC extends SQLightable> SQLiteHouseRelay update( SC o )
{ m_ctx.sendBroadcast( this.buildUpdateSignal(o) ) ; return this ; }
/**
* Constructs the {@link Intent} to be sent by {@link #update}.
* This is a separate method only so that it can be unit-tested.
* @param o an instance of the schematic class to be used as update input
* @param <SC> the schematic class
* @return the intent to be sent by {@link #update}
*/
protected <SC extends SQLightable> Intent buildUpdateSignal( SC o )
{
Intent sig = new Intent(
m_api.getFormattedKeeperAction( KEEPER_UPDATE ) ) ;
SQLightable.Reflection<SC> tbl = m_api.reflect(o) ;
sig.putExtra( m_api.getFormattedExtraTag( EXTRA_SCHEMA_CLASS_NAME ),
tbl.getTableClass().getCanonicalName() ) ;
sig.putExtra( m_api.getFormattedExtraTag( EXTRA_SCHEMA_CLASS_DATA ),
tbl.toBundle(o) ) ;
return sig ;
}
/**
* Requests deletion of a particular row in the keeper's database,
* corresponding to the schematic class instance supplied.
* @param o an instance of the schematic class
* @param <SC> the schematic class
* @return (fluid)
*/
public <SC extends SQLightable> SQLiteHouseRelay delete( SC o )
{ m_ctx.sendBroadcast( this.buildDeleteSignal(o) ) ; return this ; }
/**
* Constructs the {@link Intent} to be sent by {@link #delete}.
* This is a separate method only so that it can be unit-tested.
* @param o an instance of the schematic class to be deleted
* @param <SC> the schematic class
* @return the intent to be sent by {@link #delete}
*/
protected <SC extends SQLightable> Intent buildDeleteSignal( SC o )
{
Intent sig = new Intent(
m_api.getFormattedKeeperAction( KEEPER_DELETE ) ) ;
SQLightable.Reflection<SC> tbl = m_api.reflect(o) ;
sig.putExtra( m_api.getFormattedExtraTag( EXTRA_SCHEMA_CLASS_NAME ),
tbl.getTableClass().getCanonicalName() ) ;
sig.putExtra( m_api.getFormattedExtraTag( EXTRA_SCHEMA_CLASS_DATA ),
tbl.toBundle(o) ) ;
return sig ;
}
/**
* Requests the selection of a set of rows from the keeper's database.
* @param cls the schematic class that would contain the rows
* @param q a query against that data set
* @param <SC> the schematic class
* @return (fluid)
*/
public <SC extends SQLightable> SQLiteHouseRelay select(
Class<SC> cls, SelectionBuilder q )
{ m_ctx.sendBroadcast( this.buildSelectionSignal(cls,q) ) ; return this ; }
/**
* Constructs the {@link Intent} to be sent by {@link #select}.
* This is a separate method only so that it can be unit-tested.
* @param cls the schematic class that would contain the rows
* @param q a query against that data set
* @param <SC> the schematic class
* @return the intent to be sent by {@link #select}
*/
protected <SC extends SQLightable> Intent buildSelectionSignal(
Class<SC> cls, SelectionBuilder q )
{
Intent sig = new Intent(
m_api.getFormattedKeeperAction( KEEPER_SELECT ) ) ;
SQLightable.Reflection<SC> tbl = m_api.reflect(cls) ;
sig.putExtra( m_api.getFormattedExtraTag( EXTRA_SCHEMA_CLASS_NAME ),
tbl.getTableClass().getCanonicalName() ) ;
sig.putExtra( m_api.getFormattedExtraTag( EXTRA_SELECTION_QUERY_SPEC ),
q.toBundle() ) ;
return sig ;
}
/// Other instance methods /////////////////////////////////////////////////////
}