Project: Locast-Android
package edu.mit.mobile.android.content;
/*
 * Copyright (C) 2011  MIT Mobile Experience Lab 
 * 
 * This program is free software; you can redistribute it and/or 
 * modify it under the terms of the GNU General Public License 
 * as published by the Free Software Foundation; either version 2 
 * of the License, or (at your option) any later version. 
 * 
 * This program is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 * GNU General Public License for more details. 
 * 
 * You should have received a copy of the GNU General Public License 
 * along with this program; if not, write to the Free Software 
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. 
 */
 
import android.content.ContentProvider; 
import android.content.ContentUris; 
import android.content.ContentValues; 
import android.database.Cursor; 
import android.database.sqlite.SQLiteDatabase; 
import android.net.Uri; 
import android.provider.BaseColumns; 
 
/**
 * Database helper to make it easier to create many-to-many relationships 
 * between two arbitrary tables. 
 * 
 * <pre> 
 *     relation 
 *        ? 
 * [from] ? [to] 
 *        ? [to 2] 
 * </pre> 
 * 
 * For example, you could have an Itinerary that has a relation to multiple 
 * Casts. 
 * 
 * To use, first {@link ManyToMany#createJoinTable(SQLiteDatabase)}. Then you 
 * can create relations between between tables by 
 * {@link #addRelation(SQLiteDatabase, long, long)}. 
 * 
 * @author steve 
 * 
 */
 
public class ManyToMany { 
 public static class ManyToManyColumns implements BaseColumns { 
 
  public static final String 
  TO_ID = "to_id"
  FROM_ID = "from_id"
 
 
 public static class M2MDBHelper implements DBHelper { 
  private final String mFromTable, mToTable, mJoinTable; 
  private final Uri mToContentUri; 
 
  private final IdenticalChildFinder mIdenticalChildFinder; 
 
  /**
   * @param fromTable 
   * @param toTable 
   * @param toContentUri 
   *            will make calls to the content provider to do updates 
   *            using this URI 
   */
 
  public M2MDBHelper(String fromTable, String toTable, 
    IdenticalChildFinder identicalChildFinder, Uri toContentUri) { 
   mFromTable = fromTable; 
   mToTable = toTable; 
   mJoinTable = mFromTable + "_" + mToTable; 
 
   mToContentUri = toContentUri; 
 
   mIdenticalChildFinder = identicalChildFinder; 
  
 
  /**
   * Provides a bunch of CRUD routines for manipulating items and their relationship to one another. 
   * 
   * @param fromTable 
   * @param toTable 
   */
 
  public M2MDBHelper(String fromTable, String toTable, IdenticalChildFinder identicalChildFinder) { 
   mFromTable = fromTable; 
   mToTable = toTable; 
   mJoinTable = mFromTable + "_" + mToTable; 
 
   mToContentUri = null
 
   mIdenticalChildFinder = identicalChildFinder; 
  
 
  public String getJoinTableName(){ 
   return mJoinTable; 
  
 
  /**
   * @return the name of the from table 
   */
 
  public String getFromTable(){ 
   return mFromTable; 
  
 
  /**
   * @return the name of the to table 
   */
 
  public String getToTable() { 
   return mToTable; 
  
 
  /**
   * Generates a join table. 
   */
 
  public void createJoinTable(SQLiteDatabase db){ 
   db.execSQL("CREATE TABLE "+mJoinTable + " (" 
     + ManyToManyColumns._ID    " INTEGER PRIMARY KEY," 
     + ManyToManyColumns.TO_ID     " INTEGER" 
     // TODO foreign keys are not supported in 2.1 or below. Dynamically enable this feature. 
     // + " REFERENCES "+ mToTable + "("+BaseColumns._ID+")" 
     "," 
     + ManyToManyColumns.FROM_ID   " INTEGER" 
     ");"); 
  
 
  /**
   * Deletes the join table. 
   * 
   */
 
  public void deleteJoinTable(SQLiteDatabase db){ 
   db.execSQL("DROP TABLE IF EXISTS "+mJoinTable); 
  
 
  /**
   * Creates a link from `from' to `to'. 
   * 
   * @param db database that has the many-to-many table 
   * @param from ID of the item in the FROM table 
   * @param to ID of the item in the TO table 
   * @return ID of the newly created relation 
   */
 
  public long addRelation(SQLiteDatabase db, long from, long to){ 
   final ContentValues relation = new ContentValues(); 
   // make a many-to-many relation 
   relation.put(ManyToManyColumns.FROM_ID, from); 
   relation.put(ManyToManyColumns.TO_ID, to); 
   return db.insert(mJoinTable, null, relation); 
  
 
  /**
   * Removes all relations from a given item. 
   * 
   * @param db 
   * @param from 
   * @return the count of deleted relations 
   */
 
  public int removeRelation(SQLiteDatabase db, long from){ 
   return db.delete(mJoinTable, 
     ManyToManyColumns.FROM_ID + "=?"
     new String[]{Long.toString(from)}); 
  
 
  public int removeRelation(SQLiteDatabase db, long from, long to){ 
   return db.delete(mJoinTable, 
     ManyToManyColumns.TO_ID + "=? AND " + ManyToManyColumns.FROM_ID + "=?"
     new String[]{Long.toString(to),     Long.toString(from)}); 
  
 
  @Override 
  public Uri insertDir(SQLiteDatabase db, ContentProvider provider, 
    Uri uri, ContentValues values) { 
   return insertItemWithRelation(db, provider, uri, values, mIdenticalChildFinder); 
  
 
  /**
   * Inserts a child into the database and adds a relation to its parent. If the item described by values is already present, only adds the relation. 
   * 
   * @param db 
   * @param uri URI to insert into. This must be a be a hierarchical URI that points to the directory of the desired parent's children. Eg. "/itinerary/1/casts/" 
   * @param values values for the child 
   * @param childFinder a finder that will look for 
   * @return the URI of the child that was either related or inserted. 
   */
 
  public Uri insertItemWithRelation(SQLiteDatabase db, ContentProvider provider, Uri parentChildDir, ContentValues values, IdenticalChildFinder childFinder) { 
   final Uri parent = ProviderUtils.removeLastPathSegment(parentChildDir); 
 
   final long parentId = ContentUris.parseId(parent); 
   Uri newItem; 
 
   db.beginTransaction(); 
   try { 
 
    if (childFinder != null){ 
     newItem = childFinder.getIdenticalChild(this, parentChildDir, db, mToTable, values); 
    }else
     newItem = null
    
 
    long childId = -1
 
    // existing item found, but no relation has been established yet. 
    if (newItem != null){ 
     childId = ContentUris.parseId(newItem); 
 
    // no existing child or relation 
    }else
     if (mToContentUri != null){ 
      newItem = provider.insert(mToContentUri, values); 
      childId = ContentUris.parseId(newItem); 
     }else
      childId = db.insert(mToTable, null, values); 
      if (childId != -1){ 
       newItem = ContentUris.withAppendedId(parentChildDir, childId); 
      
     
    
 
    if (newItem != null && childId != -1){ 
     addRelation(db, parentId, childId); 
    
 
    db.setTransactionSuccessful(); 
   }finally
    db.endTransaction(); 
   
   return newItem; 
  
 
  /**
   * Updates the item in the "to" table whose URI is specified. 
   * 
   * XXX Does not verify that there's actually a relationship between from and to. 
   * 
   * @param db 
   * @param provider 
   * @param uri the URI of the child. Child uri must end in its ID 
   * @param values 
   * @param where 
   * @param whereArgs 
   * @return the number of items that have been updated 
   */
 
  @Override 
  public int updateItem(SQLiteDatabase db, ContentProvider provider, Uri uri, ContentValues values, String where, String[] whereArgs){ 
   int count; 
   if (mToContentUri != null){ 
    count = provider.update(ContentUris.withAppendedId(mToContentUri, ContentUris.parseId(uri)), values, where, whereArgs); 
   }else
    count = db.update(mToTable, values, ProviderUtils.addExtraWhere(where, BaseColumns._ID+"=?"), ProviderUtils.addExtraWhereArgs(whereArgs, uri.getLastPathSegment())); 
   
 
   return count; 
  
 
  // TODO does not yet verify a relationship. 
  @Override 
  public int updateDir(SQLiteDatabase db, ContentProvider provider, Uri uri, ContentValues values, String where, String[] whereArgs){ 
   int count; 
   if (mToContentUri != null){ 
    count = provider.update(mToContentUri, values, where, whereArgs); 
   }else
    count = db.update(mToTable, values, where, whereArgs); 
   
   return count; 
  
 
  /* (non-Javadoc)
   * @see edu.mit.mobile.android.content.DBHelper#deleteItem(android.database.sqlite.SQLiteDatabase, android.content.ContentProvider, android.net.Uri, java.lang.String, java.lang.String[]) 
   */
 
  @Override 
  public int deleteItem(SQLiteDatabase db, ContentProvider provider, Uri uri,String where, String[] whereArgs){ 
   int count; 
   try { 
    db.beginTransaction(); 
    final long childId = ContentUris.parseId(uri); 
    final Uri parent = ProviderUtils.removeLastPathSegments(uri, 2); 
 
    if (mToContentUri != null){ 
     count = provider.delete(ContentUris.withAppendedId(mToContentUri, childId), where, whereArgs); 
    }else
     count = db.delete(mToTable, ProviderUtils.addExtraWhere(where, BaseColumns._ID+"=?"), ProviderUtils.addExtraWhereArgs(whereArgs, String.valueOf(childId))); 
    
 
    final int rows = removeRelation(db, ContentUris.parseId(parent), childId); 
 
    if (rows == 0){ 
     throw new IllegalArgumentException("There is no relation between "+ parent + " and " + mToTable + ": ID "+ childId); 
    
 
    db.setTransactionSuccessful(); 
 
   }finally
    db.endTransaction(); 
   
   return count; 
  
 
  /* (non-Javadoc)
   * @see edu.mit.mobile.android.content.DBHelper##deleteDir(android.database.sqlite.SQLiteDatabase, android.content.ContentProvider, android.net.Uri, java.lang.String, java.lang.String[]) 
   */
 
  @Override 
  public int deleteDir(SQLiteDatabase db, ContentProvider provider, Uri uri,String where, String[] whereArgs){ 
   int count; 
   try { 
    db.beginTransaction(); 
    final Uri parent = ProviderUtils.removeLastPathSegment(uri); 
 
    if (mToContentUri != null){ 
     count = provider.delete(mToContentUri, where, whereArgs); 
    }else
     count = db.delete(mToTable, where, whereArgs); 
    
 
    removeRelation(db, ContentUris.parseId(parent)); 
 
    db.setTransactionSuccessful(); 
 
   }finally
    db.endTransaction(); 
   
   return count; 
  
 
 
  public static interface IdenticalChildFinder { 
   /**
    * Search the database and see if there is a child that is identical (using whatever criteria you prefer) to the one described in values. 
    * 
    * @param m2m the DBHelper for the parent/child relationship 
    * @param parentChildDir the URI of the parent's children 
    * @param db the database to do lookups on 
    * @param childTable the child table to look into 
    * @param values the values that describe the child in question. 
    * @return if an identical child is found, returns its Uri. If none are found, returns null. 
    */
 
   public Uri getIdenticalChild(DBHelper m2m, Uri parentChildDir, SQLiteDatabase db, String childTable, ContentValues values); 
  
 
  /**
   * Selects rows from the TO table that have a relation from the given item in the FROM table. 
   * 
   * @param fromId _ID of the item that's being 
   * @param db DB that contains all the tables 
   * @param toProjection projection for the TO table 
   * @param selection any extra selection query or null 
   * @param selectionArgs any extra selection arguments or null 
   * @param sortOrder the desired sort order or null 
   * @return a cursor whose content represents the to table 
   */
 
  public Cursor queryTo(long fromId, SQLiteDatabase db, String[] toProjection, String selection, String[] selectionArgs, String sortOrder){ 
   // XXX hack to get around ambiguous column names. Is there a better way to write this query? 
   if (selection != null){ 
    selection = selection.replaceAll("(\\w+=\\?)", mToTable + ".$1"); 
   
 
   return db.query(mToTable 
     " INNER JOIN " + mJoinTable 
     " ON " + mJoinTable+"."+ManyToManyColumns.TO_ID + "=" + mToTable + "." + BaseColumns._ID, 
     ProviderUtils.addPrefixToProjection(mToTable, toProjection), 
     ProviderUtils.addExtraWhere(selection, mJoinTable + "." + ManyToManyColumns.FROM_ID + "=?"), 
     ProviderUtils.addExtraWhereArgs(selectionArgs, Long.toString(fromId)), nullnull, sortOrder); 
  
 
 
  @Override 
  public Cursor queryDir(SQLiteDatabase db, Uri uri, String[] projection, 
    String selection, String[] selectionArgs, String sortOrder) { 
 
   final Uri parent = ProviderUtils.removeLastPathSegment(uri); 
 
   final long parentId = ContentUris.parseId(parent); 
   return queryTo(parentId, db, projection, selection, selectionArgs, sortOrder); 
 
  
 
  @Override 
  public Cursor queryItem(SQLiteDatabase db, Uri uri, 
    String[] projection, String selection, String[] selectionArgs, 
    String sortOrder) { 
   final Uri parent = ProviderUtils.removeLastPathSegments(uri, 2); 
 
   final long parentId = ContentUris.parseId(parent); 
 
   final String childId = uri.getLastPathSegment(); 
 
   return queryTo(parentId, 
     db, 
     projection, 
     ProviderUtils.addExtraWhere(selection, BaseColumns._ID+"=?"), 
     ProviderUtils.addExtraWhereArgs(selectionArgs, childId), 
     sortOrder); 
  
 
}