QueryBuilder.java
package net.zer0bandwidth.android.lib.content.querybuilder;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import java.util.Collection;
/**
* Builds a {@link ContentResolver} query using builder-style methods, rather
* than the standard methods, which use long lists of often-null parameters.
*
* <p>This class is supposed to do very little; the consumer should use the
* static methods {@link #insertInto}, {@link #update}, {@link #selectFrom}, and
* {@link #deleteFrom} to spawn instances of the various implementation classes
* corresponding to query actions.</p>
*
* @param <I> The implementation class which extends {@code QueryBuilder}. This
* is set explicitly in the declaration of the implementation class,
* and allows the superclass to provide concrete implementations of
* methods reusable by the descendants.
* @param <R> The return type of the {@link #execute} method, which
* corresponds to a method in {@code ContentResolver}.
*
* @since zer0bandwidth-net/android 0.1.7 (#39)
*/
public abstract class QueryBuilder<I extends QueryBuilder, R>
{
/// Inner classes //////////////////////////////////////////////////////////////
/**
* Informs a consumer of {@link QueryBuilder} that it has tried to invoke
* the {@link QueryBuilder#execute} method without first binding to a data
* source. To prevent this exception, ensure that the consumer code properly
* supplies a data source when it calls one of the static
* {@code QueryBuilder} methods that instantiates a builder.
* @since zer0bandwidth-net/android 0.1.7 (#39)
*/
public static class UnboundException
extends IllegalStateException
{
protected static final String DEFAULT_MESSAGE =
"Caller tried to execute a query without a data source reference." ;
public UnboundException()
{ super(DEFAULT_MESSAGE) ; }
public UnboundException( String sMessage )
{ super(sMessage) ; }
public UnboundException( Throwable xCause )
{ super( DEFAULT_MESSAGE, xCause ) ; }
public UnboundException( String sMessage, Throwable xCause )
{ super( sMessage, xCause ) ; }
}
/**
* Exception thrown by {@link #execute} and {@link #executeOn} when the
* underlying {@link ContentResolver} query operation fails.
* @since zer0bandwidth-net/android 0.1.7 (#39)
*/
public static class ExecutionException
extends RuntimeException
{
protected static final String DEFAULT_MESSAGE =
"Query execution failed." ;
public ExecutionException()
{ super(DEFAULT_MESSAGE) ; }
public ExecutionException( String sMessage )
{ super(sMessage) ; }
/**
* Creates a standard exception with a message indicating the class
* which failed to execute.
* @param cls the class which failed to execute
*/
public ExecutionException( Class<? extends QueryBuilder> cls )
{ super( getClassMessage(cls) ) ; }
public ExecutionException( Throwable xCause )
{ super( DEFAULT_MESSAGE, xCause ) ; }
public ExecutionException( String sMessage, Throwable xCause )
{ super( sMessage, xCause ) ; }
/**
* Creates a standard exception with a message indicating the class
* which failed to execute.
* @param cls the class which failed to execute
* @param xCause the cause of the failure
*/
public ExecutionException( Class<? extends QueryBuilder> cls, Throwable xCause )
{ super( getClassMessage(cls), xCause ) ; }
/**
* Generates an exception message based on the name of the class that
* failed.
* @param cls the class whose execution method failed
* @return a standard message indicating that the selected class failed
* to execute
*/
protected static String getClassMessage( Class<? extends QueryBuilder> cls )
{
return (new StringBuilder())
.append( cls.getSimpleName() )
.append( " execution failed." )
.toString()
;
}
}
/// Static kickoff methods (start queries of specific types) ///////////////////
/**
* Kicks off construction of an insertion query.
* When constructing the builder in this way, the caller <i>must</i> also
* call {@link #onDataSource}, or use {@link #executeOn} instead of
* {@link #execute}.
* @return an instance of the builder that handles insertion queries
*/
public static InsertionBuilder insert()
{ return new InsertionBuilder() ; }
/**
* Kicks off construction of an insertion query.
* @param rslv the resolver through which the query should be executed
* @param uri the URI at which the query should be executed
* @return an instance of the builder that handles insertion queries
* @throws UnboundException if the data context is unusable
*/
public static InsertionBuilder insertInto( ContentResolver rslv, Uri uri )
throws UnboundException
{ return new InsertionBuilder( rslv, uri ) ; }
/**
* Kicks off construction of an insertion query.
* @param ctx a context which can provide a {@link ContentResolver}
* @param uri the URI at which the query should be executed
* @return an instance of the builder that handles insertion queries
* @throws UnboundException if the data context is unusable
*/
public static InsertionBuilder insertInto( Context ctx, Uri uri )
throws UnboundException
{ return new InsertionBuilder( ctx, uri ) ; }
/**
* Kicks off construction of an update query.
* When constructing the builder in this way, the caller <i>must</i> also
* call {@link #onDataSource}, or use {@link #executeOn} instead of
* {@link #execute}.
* @return an instance of the builder that handles update queries
*/
public static UpdateBuilder update()
{ return new UpdateBuilder() ; }
/**
* Kicks off construction of an update query.
* @param rslv the resolver through which the query should be executed
* @param uri the URI at which the query should be executed
* @return an instance of the builder that handles update queries
* @throws UnboundException if the data context is unusable
*/
public static UpdateBuilder update( ContentResolver rslv, Uri uri )
throws UnboundException
{ return new UpdateBuilder( rslv, uri ) ; }
/**
* Kicks off construction of an update query.
* @param ctx a context which can provide a {@link ContentResolver}
* @param uri the URI at which the query should be executed
* @return an instance of the builder that handles update queries
* @throws UnboundException if the data context is unusable
*/
public static UpdateBuilder update( Context ctx, Uri uri )
throws UnboundException
{ return new UpdateBuilder( ctx, uri ) ; }
/**
* Kicks off construction of a selection query.
* When constructing the builder in this way, the caller <i>must</i> also
* call {@link #onDataSource}, or use {@link #executeOn} instead of
* {@link #execute}.
* @return an instance of the builder that handles selection queries
*/
public static SelectionBuilder select()
{ return new SelectionBuilder() ; }
/**
* Kicks off construction of a selection query.
* @param rslv the resolver through which the query should be executed
* @param uri the URI at which the query should be executed
* @return an instance of the builder that handles selection queries
* @throws UnboundException if the data context is unusable
*/
public static SelectionBuilder selectFrom( ContentResolver rslv, Uri uri )
throws UnboundException
{ return new SelectionBuilder( rslv, uri ) ; }
/**
* Kicks off construction of a selection query.
* @param ctx a context which can provide a {@link ContentResolver}
* @param uri the URI at which the query should be executed
* @return an instance of the builder that handles selection queries
* @throws UnboundException if the data context is unusable
*/
public static SelectionBuilder selectFrom( Context ctx, Uri uri )
throws UnboundException
{ return new SelectionBuilder( ctx, uri ) ; }
/**
* Kicks off construction of a deletion query.
* When constructing the builder in this way, the caller <i>must</i> also
* call {@link #onDataSource}, or use {@link #executeOn} instead of
* {@link #execute}.
* @return an instance of the builder that handles deletion queries
*/
public static DeletionBuilder delete()
{ return new DeletionBuilder() ; }
/**
* Kicks off construction of a deletion query.
* @param rslv the resolver through which the query should be executed
* @param uri the URI at which the query should be executed
* @return an instance of the builder that handles deletion queries
* @throws UnboundException if the data context is unusable
*/
public static DeletionBuilder deleteFrom( ContentResolver rslv, Uri uri )
throws UnboundException
{ return new DeletionBuilder( rslv, uri ) ; }
/**
* Kicks off construction of a deletion query.
* @param ctx a context which can provide a {@link ContentResolver}
* @param uri the URI at which the query should be executed
* @return an instance of the builder that handles deletion queries
* @throws UnboundException if the data context is unusable
*/
public static DeletionBuilder deleteFrom( Context ctx, Uri uri )
throws UnboundException
{ return new DeletionBuilder( ctx, uri ) ; }
/// Other static methods ///////////////////////////////////////////////////////
/**
* Obtains a {@link ContentResolver} from the specified context, throwing an
* exception if that context is null.
* @param ctx a context which can provide a content resolver.
* @return the content resolver for the specified context
* @throws UnboundException if the context is null
*/
protected static ContentResolver getContentResolver( Context ctx )
throws UnboundException
{
if( ctx == null )
throw new UnboundException( "Null context cannot provide resolver." ) ;
return ctx.getContentResolver() ;
}
/**
* Ensures that the specified {@link ContentResolver} and {@link Uri} are
* non-null and usable as a data context.
* @throws ExecutionException if any problems occur
*/
protected static void validateDataContextBinding( ContentResolver rslv,
Uri uri )
throws QueryBuilder.UnboundException
{
if( rslv == null )
throw new UnboundException( "A content resolver is required." ) ;
if( uri == null )
throw new UnboundException( "A valid URI is required." ) ;
}
/// Shared member fields ///////////////////////////////////////////////////////
/** The {@link ContentResolver} to which the builder is bound. */
protected ContentResolver m_rslv = null ;
/** The {@link Uri} to be supplied to the {@link ContentResolver}. */
protected Uri m_uri = null ;
/**
* For "insert" and "update" operations, these are the values to be written.
*/
protected ContentValues m_valsToWrite = null ;
/**
* A substitute, explicit "where" format string, for which
* {@link #m_asExplicitWhereParams} provides the values.
*/
protected String m_sExplicitWhereFormat = null ;
/**
* A substitute, explicit list of "where" parameters, to fit the format
* string stored in {@link #m_sExplicitWhereFormat}.
*/
protected String[] m_asExplicitWhereParams = null ;
/// Shared constructors ////////////////////////////////////////////////////////
/**
* The default constructor; does not bind to a particular data source.
*/
public QueryBuilder() {}
/**
* A shared constructor which binds the builder to a the resolver found in
* the given context, and the specified URI.
* @param ctx a context which can provide a {@link ContentResolver}
* @param uri the URI at which the query should be executed
* @throws QueryBuilder.UnboundException if either parameter is unusable
*/
public QueryBuilder( Context ctx, Uri uri )
throws QueryBuilder.UnboundException
{ this.onDataSource( ctx, uri ) ; }
/**
* A shared constructor which binds the builder to a specific resolver and
* URI.
* @param rslv the resolver through which the query should be executed
* @param uri the URI at which the query should be executed
* @throws QueryBuilder.UnboundException if either parameter is unusable
*/
public QueryBuilder( ContentResolver rslv, Uri uri )
throws QueryBuilder.UnboundException
{ this.onDataSource( rslv, uri ) ; }
/// Shared methods /////////////////////////////////////////////////////////////
/**
* Binds the builder to a specific data context, to be used by the execution
* methods.
* @param rslv the resolver through which the query should be executed
* @param uri the URI at which the query should be executed
* @return (fluid)
* @throws QueryBuilder.UnboundException if either parameter is unusable
*/
public I onDataSource( ContentResolver rslv, Uri uri )
throws QueryBuilder.UnboundException
{
validateDataContextBinding( rslv, uri ) ;
m_rslv = rslv ;
m_uri = uri ;
//noinspection unchecked - guaranteed by generic parameterization
return (I)this ;
}
/**
* Binds the builder to a specific data context, to be used by the execution
* methods.
* @param ctx a context which can provide a {@link ContentResolver}
* @param uri the URI at which the query should be executed
* @return (fluid)
* @throws QueryBuilder.UnboundException if either parameter is unusable
*/
public I onDataSource( Context ctx, Uri uri )
throws QueryBuilder.UnboundException
{ return this.onDataSource( getContentResolver(ctx), uri ) ; }
/**
* Sets values to be written as part of an "insert" or "update" operation,
* if applicable.
* @param vals the values to be written
* @return (fluid)
*/
public I setValues( ContentValues vals )
{
m_valsToWrite = vals ;
//noinspection unchecked - guaranteed by generic parameterization
return (I)this ;
}
/**
* Constructs an explicit "where" clause for a query.
*
* <p>The supplied string is used as-is in the underlying
* {@link ContentResolver} query function, and should contain only necessary
* columns with literal values (no variable substitutions). Based on this
* restriction, the collection of variable substitution sources will be set
* as {@code null}.</p>
* @param sWhereClause the explicit "where" clause, containing only literal
* values
* @return (fluid)
*/
public I where( String sWhereClause )
{
m_sExplicitWhereFormat = sWhereClause ;
m_asExplicitWhereParams = null ;
//noinspection unchecked - guaranteed by generic parameterization
return (I)this ;
}
/**
* Sets the "where" clause format and values for a query.
* @param sWhereFormat the format string of the "where" clause, which must
* use {@code ?} for parameter substitution
* @param asWhereParams the parameters for the "where" clause, assigned to
* substitution markers in the format string
* @return (fluid)
* @see net.zer0bandwidth.android.lib.content.ContentUtils#QUERY_VARIABLE_MARKER
*/
public I where( String sWhereFormat, String... asWhereParams )
{
m_sExplicitWhereFormat = sWhereFormat ;
m_asExplicitWhereParams = asWhereParams ;
//noinspection unchecked - guaranteed by generic parameterization
return (I)this ;
}
/**
* Sets the "where" clause format and values for a query.
* @param sWhereFormat the format string of the "where" clause, which must
* use {@code ?} for parameter substitution
* @param asWhereParams the parameters for the "where" clause, assigned to
* substitution markers in the format string
* @return (fluid)
* @see net.zer0bandwidth.android.lib.content.ContentUtils#QUERY_VARIABLE_MARKER
*/
public I where( String sWhereFormat, Collection<String> asWhereParams )
{
if( asWhereParams == null )
return this.where( sWhereFormat ) ;
else
{
return this.where( sWhereFormat,
asWhereParams.toArray( new String[asWhereParams.size()] ) ) ;
}
}
/**
* Creates the Android "where" clause format string to be passed to a
* {@link ContentResolver} query method.
* @return the "where" clause format string
*/
protected String getWhereFormat()
{ return m_sExplicitWhereFormat ; }
/**
* Creates the array of "where" clause value substitutions to be passed to a
* {@link ContentResolver} query method.
* @return the "where" clause format string's parameters
*/
protected String[] getWhereParams()
{ return m_asExplicitWhereParams ; }
/**
* Executes the query that has been built by the implementation class, using
* the {@link ContentResolver} and {@link Uri} to which the builder has been
* bound, either by the constructor, or by an invocation of
* {@link #onDataSource}.
* @return the return type appropriate to the query action
*/
public final R execute()
throws UnboundException, ExecutionException
{ return this.executeOn( this.m_rslv, this.m_uri ) ; }
/**
* Executes the query that has been built by the implementation class, using
* the supplied {@link ContentResolver} and {@link Uri}. Usually, this is
* not invoked directly, but is instead consumed by {@link #execute}.
* @param rslv the resolver through which the query should be executed
* @param uri the URI at which the query should be executed
* @return the return type appropriate to the query action
* @throws UnboundException if the data context binding is inadequate
* @throws ExecutionException if the underlying query fails
*/
public final R executeOn( ContentResolver rslv, Uri uri )
throws UnboundException, ExecutionException
{
validateDataContextBinding( rslv, uri ) ;
try { return this.executeQuery( rslv, uri ) ; }
catch( Exception x )
{ throw new ExecutionException( this.getClass(), x ) ; }
}
/**
* Executes the query that has been built by the implementation class, using
* the {@link ContentResolver} provided by the specified {@link Context},
* and the specified {@link Uri}.
* @param ctx a context which can provide a {@link ContentResolver}
* @param uri the URI at which the query should be executed
* @return the return type appropriate to the query action
* @throws UnboundException if the data context binding is inadequate
* @throws ExecutionException if the underlying query fails
*/
public final R executeOn( Context ctx, Uri uri )
throws UnboundException, ExecutionException
{ return this.executeOn( getContentResolver(ctx), uri ) ; }
/// Abstract class specification ///////////////////////////////////////////////
/**
* Consumed by {@link #execute} and {@link #executeOn} to actually carry out
* the operation. In the implementation class, this method should consist
* solely of the {@link ContentResolver} query method call, plus any other
* pre-checks which might be able to short-circuit the query execution.
* @param rslv the resolver through which the query should be executed
* @param uri the URI at which the query should be executed
* @return the return type appropriate to the query action
* @throws Exception if anything goes wrong
*/
protected abstract R executeQuery( ContentResolver rslv, Uri uri )
throws Exception ;
}