QueryBuilder.java

package net.zer0bandwidth.android.lib.database.querybuilder;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;

import net.zer0bandwidth.android.lib.database.SQLitePortal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

import static net.zer0bandwidth.android.lib.database.SQLiteSyntax.SQLITE_VAR;

/**
 * Builds a SQLite query using methods, rather than the methods from the
 * {@link SQLiteDatabase} methods that use long lists of 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 database actions ({@code INSERT}, {@code UPDATE},
 * {@code SELECT}, and {@code DELETE}).</p>
 *
 * <p>In its initial implementation, the class provides methods to set an
 * explicit format string and parameter list for the {@code WHERE} clause,
 * similar to the syntax used by the various Android functions in
 * {@link SQLiteDatabase}. In the future, the API of this class will be extended
 * to provide a grammar for constructing the conditional statement through
 * builder methods.</p>
 *
 * <p>The class builds on the foundation of {@link SQLitePortal} and derives
 * several of its static constants and design decisions from features of that
 * class.</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
 *           shared methods.
 * @param <R> The return type of the underlying {@code SQLiteDatabase} function.
 *           This is set explicitly in the declaration of the implementation
 *           class, and will be the return type of {@link #execute} and
 *           {@link #executeOn}.
 *
 * @since zer0bandwidth-net/android 0.1.1 (#20)
 */
@SuppressWarnings( "unused" )                              // This is a library.
public abstract class QueryBuilder<I extends QueryBuilder, R>
{
/// Inner Classes //////////////////////////////////////////////////////////////

	/**
	 * Informs a consumer that the builder's {@link #execute} method was invoked
	 * while the builder was not bound to a target database instance. When
	 * encountering this exception in code that consumes {@code QueryBuilder},
	 * ensure that the code either uses one of the two-argument kickoff methods
	 * that includes a database binding, or uses {@link #onDatabase} to
	 * define a binding, or uses {@link #executeOn} instead of {@link #execute}.
	 * @since zer0bandwidth-net/android 0.1.4 (#37)
	 */
	public static class UnboundException
	extends IllegalStateException
	{
		protected static final String DEFAULT_MESSAGE =
			"Caller tried to execute a query without a database 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 ) ; }
	}

/// Static constants ///////////////////////////////////////////////////////////

	// Last one was removed in 0.2.0 (#49). Placeholder space remains.

/// Static kickoff methods (starts a query of a given type) ////////////////////

	/**
	 * Kicks off construction of an {@code INSERT} query.
	 * @param sTableName the name of the table into which rows will be inserted
	 * @return an instance of a builder that can handle insertion queries
	 */
	public static InsertionBuilder insertInto( String sTableName )
	{ return new InsertionBuilder( sTableName ) ; }

	/**
	 * Kicks off construction of an {@code INSERT} query, bound to a specific
	 * database instance.
	 * @param db the database on which the query should be executed
	 * @param sTableName the name of the table into which rows will be inserted
	 * @return an instance of a builder that can handle insertion queries
	 * @since zer0bandwidth-net/android 0.1.4 (#37)
	 */
	public static InsertionBuilder insertInto( SQLiteDatabase db, String sTableName )
	{ return insertInto(sTableName).onDatabase(db) ; }

	/**
	 * Kicks off construction of an {@code UPDATE} query.
	 * @param sTableName the name of the table in which rows will be updated
	 * @return an instance of a builder that can handle update queries
	 */
	public static UpdateBuilder update( String sTableName )
	{ return new UpdateBuilder( sTableName ) ; }

	/**
	 * Kicks off construction of an {@code UPDATE} query, bound to a specific
	 * database instance.
	 * @param db the database on which the query should be executed
	 * @param sTableName the name of the table in which rows will be updated
	 * @return an instance of a builder that can handle update queries
	 * @since zer0bandwidth-net/android 0.1.4 (#37)
	 */
	public static UpdateBuilder update( SQLiteDatabase db, String sTableName )
	{ return update(sTableName).onDatabase(db) ; }

	/**
	 * Kicks off construction of a {@code SELECT} query.
	 * @param sTableName the name of the table from which rows will be selected
	 * @return an instance of a builder that can handle selection queries
	 */
	public static SelectionBuilder selectFrom( String sTableName )
	{ return new SelectionBuilder( sTableName ) ; }

	/**
	 * Kicks off construction of a {@code SELECT} query, bound to a specific
	 * database instance.
	 * @param db the database on which the query should be executed
	 * @param sTableName the name of the table from which rows will be selected
	 * @return an instance of a builder that can handle selection queries
	 * @since zer0bandwidth-net/android 0.1.4 (#37)
	 */
	public static SelectionBuilder selectFrom( SQLiteDatabase db, String sTableName )
	{ return selectFrom(sTableName).onDatabase(db) ; }

	/**
	 * Kicks off construction of a {@code DELETE} query.
	 * @param sTableName the name of the table from which rows will be deleted
	 * @return an instance of a builder that can handle deletion queries
	 */
	public static DeletionBuilder deleteFrom( String sTableName )
	{ return new DeletionBuilder( sTableName ) ; }

	/**
	 * Kicks off construction of a {@code DELETE} query, bound to a specific
	 * database instance.
	 * @param db the database on which the query should be executed
	 * @param sTableName the name of the table from which rows will be deleted
	 * @return an instance of a builder that can handle deletion queries
	 * @since zer0bandwidth-net/android 0.1.4 (#37)
	 */
	public static DeletionBuilder deleteFrom( SQLiteDatabase db, String sTableName )
	{ return deleteFrom(sTableName).onDatabase(db) ; }

/// Other static methods ///////////////////////////////////////////////////////

	/**
	 * Renders the key/value pairs in a set of {@link ContentValues} as a list
	 * of input parameters to an SQL {@code INSERT} or {@code UPDATE} query's
	 * {@code SET} clause.
	 *
	 * So that the fields always appear in consistent order (rather than by hash
	 * code), the method will sort lexically by key before rendering the output
	 * string.
	 *
	 * @param vals the key/value pairs to be rendered
	 * @return an SQL {@code SET} clause body
	 */
	public static String toSQLInputParams( ContentValues vals )
	{
		if( vals == null || vals.size() == 0 ) return "" ; // trivially

		StringBuilder sb = new StringBuilder() ;
		List<Map.Entry<String,Object>> aEntries =
				new ArrayList<>( vals.valueSet() ) ;
		Collections.sort( aEntries, new Comparator<Map.Entry<String,Object>>()
		{
			@Override
			public int compare( Map.Entry<String,Object> lhs, Map.Entry<String,Object> rhs )
			{ return (lhs.getKey()).compareTo(rhs.getKey()) ; }
		});
		for( Map.Entry<String,Object> pair : aEntries )
		{
			if( sb.length() > 0 ) sb.append( ", " ) ;
			sb.append( pair.getKey() ).append( "=" ) ;
			if( pair.getValue() instanceof Number )
				sb.append( pair.getValue() ) ;
			else
				sb.append( "'" ).append( pair.getValue() ).append( "'" ) ;
		}
		return sb.toString() ;
	}

/// Shared member fields ///////////////////////////////////////////////////////

	/** The name of the table on which the query will operate. */
	protected String m_sTableName ;

	/**
	 * For {@code INSERT} and {@code DELETE} operations, these are the values to
	 * be written.
	 */
	protected ContentValues m_valsToWrite = null ;

	/**
	 * A substitute, explicit format string for a {@code WHERE} clause, for
	 * which {@link #m_asExplicitWhereParams} provides the values.
	 */
	protected String m_sExplicitWhereFormat = null ;

	/**
	 * A substitute, explicit list of parameters for the {@code WHERE} clause
	 * format specified in {@link #m_sExplicitWhereFormat}.
	 */
	protected String[] m_asExplicitWhereParams = null ;

	/**
	 * A persistent binding to a specific database, used by {@link #execute}.
	 * @since zer0bandwidth-net/android 0.1.4 (#37)
	 */
	protected SQLiteDatabase m_dbTarget = null ;

/// Shared constructor /////////////////////////////////////////////////////////

	/**
	 * Superclass's constructor, which initializes the shared member fields.
	 * @param sTableName the name of the table on which the query will be
	 *                   performed
	 */
	public QueryBuilder( String sTableName )
	{
		m_sTableName = sTableName ;
	}

/// Shared methods /////////////////////////////////////////////////////////////

	/**
	 * Binds the builder to a specific database instance, to be used by
	 * {@link #execute}.
	 * @param db the database instance on which the query should be executed
	 * @return (fluid)
	 * @since zer0bandwidth-net/android 0.1.4 (#37)
	 */
	@SuppressWarnings( "unchecked" )
	public I onDatabase( SQLiteDatabase db )
	{ m_dbTarget = db ; return (I)this ; }

	/**
	 * Sets the table name in which the query will be executed.
	 * @param sTableName the name of the table
	 * @return (fluid)
	 */
	@SuppressWarnings( "unchecked" )
	protected I setTableName( String sTableName )
	{ m_sTableName = sTableName ; return (I)this ; }

	/**
	 * Sets values to be written as part of an {@code INSERT} or {@code UPDATE}
	 * query.
	 * @param vals the values to be written
	 * @return (fluid)
	 */
	@SuppressWarnings( "unchecked" )
	public I setValues( ContentValues vals )
	{
		m_valsToWrite = vals ;
		return (I)this ;
	}

	/**
	 * Constructs an explicit {@code WHERE} clause for the query.
	 *
	 * The supplied string is used as a format string for the {@code WHERE}
	 * clause and should contain <b>no</b> variable substitutions. The
	 * collection of variable values will be set to {@code null} by this method,
	 * based on this assumption.
	 *
	 * @param sWhereClause the explicit {@code WHERE} clause, containing
	 *  <b>no</b> variable substitution placeholders
	 * @return (fluid)
	 */
	@SuppressWarnings( "unchecked" )
	public I where( String sWhereClause )
	{
		m_sExplicitWhereFormat = sWhereClause ;
		m_asExplicitWhereParams = null ;
		return (I)this ;
	}

	/**
	 * Constructs an explicit {@code WHERE} clause for the query.
	 * @param sWhereFormat the format string for the {@code WHERE} clause; uses
	 *                     {@code ?} for parameter substitution
	 * @param asWhereParams the parameters for the {@code WHERE} clause
	 * @return (fluid)
	 */
	@SuppressWarnings( "unchecked" )
	public I where( String sWhereFormat, String... asWhereParams )
	{
		m_sExplicitWhereFormat = sWhereFormat ;
		m_asExplicitWhereParams = asWhereParams ;
		return (I)this ;
	}

	/**
	 * Constructs an explicit {@code WHERE} clause for the query.
	 * @param sWhereFormat the format string for the {@code WHERE} clause; uses
	 *                     {@code ?} for parameter substitution
	 * @param asWhereParams the parameters for the {@code WHERE} clause
	 * @return (fluid)
	 */
	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 {@code WHERE} clause template to be passed to a
	 * {@link SQLiteDatabase} function.
	 * @return the {@code WHERE} clause template
	 */
	protected String getWhereFormat()
	{
		if( m_sExplicitWhereFormat != null )
			return m_sExplicitWhereFormat ;

		return null ;
	}

	/**
	 * Creates the array of {@code WHERE} clause value substitutions to be
	 * passed to a {@link SQLiteDatabase} function.
	 * @return the {@code WHERE} clause template parameters
	 */
	protected String[] getWhereParams()
	{
		if( m_sExplicitWhereFormat != null )
			return m_asExplicitWhereParams ; // even if THEY are null

		return null ;
	}

	/**
	 * Creates a raw SQLite {@code WHERE} clause based on the format and params
	 * created for the instance.
	 * @return a raw {@code WHERE} clause
	 */
	protected String getWhereClause()
	{
		if( m_sExplicitWhereFormat == null ) return null ;
		if( ! m_sExplicitWhereFormat.contains( SQLITE_VAR ) )
			return m_sExplicitWhereFormat ;        // Contains no substitutions.
		if( m_asExplicitWhereParams == null || m_asExplicitWhereParams.length == 0 )
		{
			throw new IllegalStateException(
					"Need parameters but don't have them." ) ;
		}
		String sFormat = m_sExplicitWhereFormat.replace( SQLITE_VAR, "%s" ) ;
		return String.format( sFormat, ((Object[])(m_asExplicitWhereParams)) ) ;
	}

/// Abstract class specification ///////////////////////////////////////////////

	/**
	 * Executes the query that has been built by the implementation class.
	 * @param db the database instance on which the query should be executed.
	 * @return the usual return value of the underlying method
	 */
	public abstract R executeOn( SQLiteDatabase db ) ;

	/**
	 * Executes the query that has been built by the implementation class, on
	 * the database instance to which the builder has been bound, either by a
	 * constructor, or by {@link #onDatabase}.
	 * @return the usual return value of the underlying method
	 * @throws QueryBuilder.UnboundException if the builder is not yet bound to
	 *  a database instance
	 * @since zer0bandwidth-net/android 0.1.4 (#37)
	 */
	public final R execute()
	throws QueryBuilder.UnboundException
	{
		if( m_dbTarget == null )
			throw new QueryBuilder.UnboundException() ;

		return this.executeOn( m_dbTarget ) ;
	}
}