/** * Copyright (c) 2013, Sana * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Sana nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL Sana BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.sana.android.db; import java.text.ParseException; import java.util.Date; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.UUID; import org.sana.android.content.Uris; import org.sana.android.provider.BaseContract; import org.sana.api.IModel; import org.sana.util.DateUtil; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.CursorWrapper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; public abstract class ModelWrapper<T extends IModel> extends CursorWrapper implements ModelIterable<T>, IModel { public static final String TAG = ModelWrapper.class.getSimpleName(); public static interface BaseProjection{ public static String[] ID_PROJECTION = new String[] { BaseContract._ID }; public static String[] UUID_PROJECTION = new String[] { BaseContract._ID }; } public ModelWrapper(Cursor cursor){ super(cursor); } public boolean getBooleanField(int columnIndex){ boolean field = false; field = (getInt(columnIndex) == 1); return field; } public Date getDateField(String field){ String dateStr = getString(getColumnIndex(field)); try { return DateUtil.parseDate(dateStr); } catch (ParseException e) { throw new IllegalArgumentException("date string=" + dateStr); } } public int getIntField(String field){ return getInt(getColumnIndex(field)); } public String getStringField(String field){ return getString(getColumnIndex(field)); } public boolean getBooleanField(String field){ return getBooleanField(getColumnIndex(field)); } /* (non-Javadoc) * @see org.sana.api.IModel#getUuid() */ @Override public String getUuid() { return getStringField(BaseContract.UUID); } /* (non-Javadoc) * @see org.sana.api.IModel#getCreated() */ @Override public Date getCreated() { return getDateField(BaseContract.CREATED); } /* (non-Javadoc) * @see org.sana.api.IModel#getModified() */ @Override public Date getModified() { return getDateField(BaseContract.MODIFIED); } /* * (non-Javadoc) * @see org.sana.android.db.ModelIterable#next() */ public T next() { if(moveToNext()){ try{ return getObject(); } catch (Exception e){ throw new NoSuchElementException("Failed to instantiate object "); } } throw new NoSuchElementException("Wrapped cursor at or past last."); } /** * Should create a new instance of <T> from the values in the current row. * Note: Only columns declared as part of the projection in the original * query will be non null in the instance. * @return */ public abstract T getObject(); /* (non-Javadoc) * @see java.util.Iterator#hasNext() */ public boolean hasNext() { return !isLast(); } /* (non-Javadoc) * @see java.util.Iterator#remove() */ public void remove() { throw new UnsupportedOperationException("Removal not supported"); } /* (non-Javadoc) * @see java.lang.Iterable#iterator() */ @Override public Iterator<T> iterator() { moveToFirst(); return new ProxyIterator<T>(this); } public java.util.List<T> toList(ModelWrapper<T> wrapper){ java.util.List<T> list = new java.util.ArrayList<T>(wrapper.getCount()); for(T t:wrapper){ list.add(t); } return list; } public static class ProxyIterator<T extends IModel> implements Iterator <T> { private ModelWrapper<T> modelWrapper; public ProxyIterator(ModelWrapper<T> proxy){ this.modelWrapper = proxy; } /* (non-Javadoc) * @see java.util.Iterator#hasNext() */ public boolean hasNext() { return modelWrapper.hasNext(); } /* (non-Javadoc) * @see java.util.Iterator#next() */ public T next() { return modelWrapper.next(); } /* (non-Javadoc) * @see java.util.Iterator#remove() */ public void remove() { throw new UnsupportedOperationException("Removal not supported"); } } public static synchronized String constructSelectionClause(String[] fields){ StringBuilder selection = new StringBuilder(); int index = 0; for(String field:fields){ if(index > 0) selection.append(" AND "); selection.append(field + " = ?"); index++; } return selection.toString(); } /** * Convenience wrapper which returns a cursor representing a single row * selected a single column value. * * @param contentUri The content style Uri to query * @param resolver The resolver which will perform the query. * @param field The field, or column, to select by. * @return A cursor with a single row. * @throws IllegalArgumentException if multiple rows are returned. */ public static synchronized Cursor getOneByField(Uri contentUri, ContentResolver resolver, String field, Object object) { String selection = field + " = ?"; Cursor cursor = resolver.query(contentUri,null, selection, new String[]{ object.toString() }, null); if(cursor != null && cursor.getCount() > 1){ cursor.close(); throw new IllegalArgumentException("Multiple entries found! Expecting one."); } return cursor; } /** * Convenience wrapper which returns a cursor representing a single row * selected a single column value. * * @param contentUri The content style Uri to query * @param resolver The resolver which will perform the query. * @param fields The field, or column, to select by. * @param vals The selection argument or, row value, to select by. * @return A cursor with a single row. * @throws IllegalArgumentException if multiple rows are returned. */ public static synchronized Cursor getOneByFields(Uri contentUri, ContentResolver resolver, String[] fields, String[] vals) { Cursor cursor = ModelWrapper.getAllByFields(contentUri, resolver, fields, vals); if(cursor != null && cursor.getCount() > 1){ cursor.close(); throw new IllegalArgumentException("Multiple entries found! Expecting one."); } return cursor; } /** * Convenience wrapper to returns a cursor representing a single row matched * by the uuid value. * * @param contentUri The content style Uri to query * @param resolver The resolver which will perform the query. * @param uuid The uuid to select by. * @return A cursor with a single row. * @throws IllegalArgumentException if multiple rows are returned. */ public static synchronized Cursor getOneByUuid(Uri contentUri, ContentResolver resolver, String uuid) { String selection = BaseContract.UUID + " = ?"; Cursor cursor = resolver.query(contentUri,null, selection, new String[]{ uuid }, null); if(cursor != null && cursor.getCount() > 1){ cursor.close(); throw new IllegalArgumentException("Non unique id! " +contentUri+"/"+uuid); } return cursor; } /** * Convenience wrapper which returns a cursor representing zero or more rows * selected a single column value. * * @param contentUri The content style Uri to query * @param resolver The resolver which will perform the query. * @param field The field, or column, to select by. * @param object The selection argument or, row value, to select by. * @return A cursor with zero or more rows. */ public static synchronized Cursor getAllByField(Uri contentUri, ContentResolver resolver, String field, Object object) { return ModelWrapper.getAllByFieldOrdered(contentUri, resolver, field, object, null); } /** * Convenience wrapper which returns a cursor representing zero or more rows * selected a single column value. * * @param contentUri The content style Uri to query * @param resolver The resolver which will perform the query. * @param fields The field, or column, to select by. * @param vals The selection argument or, row value, to select by. * @return A cursor with zero or more rows. */ public static synchronized Cursor getAllByFields(Uri contentUri, ContentResolver resolver, String[] fields, String[] vals) { return ModelWrapper.getAllByFieldsOrdered(contentUri, resolver, fields, vals, null); } /** * Convenience wrapper which returns a cursor representing zero or more rows * selected a single column value and ordered. Passing a null field or object * will bypass any selection. * * @param contentUri The content style Uri to query * @param resolver The resolver which will perform the query. * @param field The field, or column, to select by. * @param object The selection argument or, row value, to select by. * @paeam order The order to return by. * @return A cursor with zero or more rows. */ public static synchronized Cursor getAllByFieldOrdered(Uri contentUri, ContentResolver resolver, String field, Object object, String order) { String selection = null; String[] selectionArgs = null; if(!TextUtils.isEmpty(field) && object != null){ selection = field + " = ?"; selectionArgs = new String[]{ object.toString() } ; } return resolver.query(contentUri,null, selection, selectionArgs, order); } /** * Convenience wrapper which returns a cursor representing zero or more rows * selected a single column value and ordered. Passing a null field or object * will bypass any selection. * * @param contentUri The content style Uri to query * @param resolver The resolver which will perform the query. * @param fields The field, or column, to select by. * @param vals The selection argument or, row values, to select by. * @paeam order The order to return by. * @return A cursor with zero or more rows. */ public static synchronized Cursor getAllByFieldsOrdered(Uri contentUri, ContentResolver resolver, String[] fields, String[] vals, String order) { StringBuilder selection = new StringBuilder(); int index = 0; for(String field:fields){ if(index > 0) selection.append(" AND "); selection.append(field + " = ?"); index++; } return resolver.query(contentUri, null, selection.toString(), vals, order); } /** * Convenience wrapper to return a cursor which returns all of the entries * ordered by {@link org.sana.android.provider.BaseContract#CREATED CREATED} * in ascending order, or, oldest first. * * @param contentUri The content style Uri to query * @param resolver The resolver which will perform the query. * @return A cursor with the result or null. */ public static synchronized Cursor getAllByCreatedAsc(Uri contentUri, ContentResolver resolver) { return ModelWrapper.getAllByFieldOrdered(contentUri, resolver, null, null, BaseContract.CREATED +" ASC"); } /** * Convenience wrapper to return a cursor which returns all of the entries * ordered by {@link org.sana.android.provider.BaseContract#CREATED CREATED} * in descending order, or, newest first. * * @param contentUri The content style Uri to query * @param resolver The resolver which will perform the query. * @return A cursor with the result or null. */ public static synchronized Cursor getAllByCreatedDesc(Uri contentUri, ContentResolver resolver) { final String order = BaseContract.CREATED +" DESC"; return resolver.query(contentUri,null, null,null, order); } /** * Convenience wrapper to return a cursor which returns all of the entries * ordered by {@link org.sana.android.provider.BaseContract#MODIFIED MODIFIED} * in ascending order, or, oldest first. * * @param contentUri The content style Uri to query * @param resolver The resolver which will perform the query. * @return A cursor with the result or null. */ public static synchronized Cursor getAllByModifiedAsc(Uri contentUri, ContentResolver resolver) { final String order = BaseContract.MODIFIED +" ASC"; return resolver.query(contentUri,null, null,null, order); } /** * Convenience wrapper to return a cursor which returns all of the entries * ordered by {@link org.sana.android.provider.BaseContract#MODIFIED MODIFIED} * in ascending order, or, newest first. * * @param contentUri The content style Uri to query * @param resolver The resolver which will perform the query. * @return A cursor with the result or null. */ public static synchronized Cursor getAllByModifiedDesc(Uri contentUri, ContentResolver resolver) { final String order = BaseContract.MODIFIED +" DESC"; return resolver.query(contentUri,null, null,null, order); } /** * Creates or updates an entry. * * @param uri * @param values * @param resolver * @return */ public static synchronized boolean insertOrUpdate(Uri uri, ContentValues values, ContentResolver resolver){ Cursor c = null; boolean exists = false; String uuid = (values.containsKey(BaseContract.UUID))? values.getAsString(BaseContract.UUID): null; if(uuid != null){ try{ c = ModelWrapper.getOneByUuid(uri, resolver, uuid); if(c != null && c.moveToFirst() && c.getCount() == 1){ exists = true; } } catch(Exception e){ e.printStackTrace(); } finally { if(c!=null) c.close(); } } try{ if(exists){ if(values.containsKey(BaseContract.UUID)) values.remove(BaseContract.UUID); resolver.update(uri, values, null, null); } else { resolver.insert(uri, values); } } catch (Exception e){ e.printStackTrace(); return false; } return true; } public synchronized static Uri getOneReferenceByFields(Uri contentUri, String[] fields, String[] vals, ContentResolver resolver) { Uri uri = contentUri; Cursor c = null; try{ c = ModelWrapper.getOneByFields(contentUri, resolver, fields, vals); if(c != null && c.moveToFirst() && c.getCount() == 1){ long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); uri = ContentUris.withAppendedId(contentUri, id); } }catch(Exception e){ e.printStackTrace(); } finally { if(c!=null) c.close(); } return uri; } /** * Returns whether an item is unique and exists in the database * @param resolver * @param uri * @param selection * @param selectionArgs * @return */ public static synchronized Uri exists(ContentResolver resolver, Uri uri, String selection, String[] selectionArgs) { Uri result = null; Cursor c = null; boolean exists = false; // allows for appending uuid query if(selection == null && selectionArgs == null && !TextUtils.isEmpty(uri.getQuery())){ String uuid = uri.getQueryParameter(BaseContract.UUID); selection = BaseContract.UUID + "=?"; selectionArgs = new String[]{ uuid }; } try{ c = resolver.query(uri, BaseProjection.ID_PROJECTION, selection, selectionArgs, null); if(c != null && c.moveToFirst() && c.getCount() == 1){ exists = true; } } catch(Exception e){ e.printStackTrace(); throw new IllegalArgumentException(e); } finally { if(c!=null) c.close(); } return result; } /** * Returns whether an item is unique and exists in the database * * @param context * @param uri * @param selection * @param selectionArgs * @return */ public static synchronized boolean exists(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; boolean exists = false; boolean unique = false; if(Uris.isEmpty(uri)) throw new NullPointerException("Empty object Uri."); try { // Do the query cursor = context.getContentResolver().query(uri, BaseProjection.ID_PROJECTION, selection, selectionArgs, null); // Check that the cursor returned if (cursor != null){ if (cursor.getCount() == 1){ // Count was one so exists and unique exists = true; unique = true; } else if (cursor.getCount() > 1) { exists = true; } } } catch (Exception e) { e.printStackTrace(); } finally { if(cursor != null) cursor.close(); } // Handle the existence and uniqueness constraints if(exists && !unique) { // TODO Class based exception throw new IllegalArgumentException("MultipleObjectsReturned"); } return exists; } /** * Validates the existence of an object in the database when passed a * Uri with the row id or object uuid as the last path segment. * * @param context * @param uri * @return */ public static synchronized boolean exists(Context context, Uri uri) { switch(Uris.getTypeDescriptor(uri)) { case Uris.ITEM_UUID: case Uris.ITEM_ID: return exists(context,uri,null,null); case Uris.ITEMS: default: throw new IllegalArgumentException("Invalid Uri. Directory type." + uri); } } /** * Inserts or updates and returns * @param uri * @param values * @param resolver * @return */ public static synchronized Uri getOrCreate(Uri uri, ContentValues values, ContentResolver resolver) { Uri result = Uri.EMPTY; Cursor c = null; int updated = 0; boolean created = false; switch(Uris.getTypeDescriptor(uri)){ case Uris.ITEM_UUID: case Uris.ITEM_ID: if(Uris.isEmpty(exists(resolver, uri, null, null))){ updated = resolver.update(uri, values, null, null); } else { throw new IllegalArgumentException("Error updating. Item does not exist: " + uri); } break; case Uris.ITEMS: if(uri.getQuery() != null){ String uuid = uri.getQueryParameter(BaseContract.UUID); String selection = BaseContract.UUID + "=?"; String[] selectionArgs = new String[]{ uuid }; result = exists(resolver, uri, selection, selectionArgs); if(!Uris.isEmpty(result)){ resolver.update(uri, values, null, null); } else { result = resolver.insert(uri, values); } } else { result = resolver.insert(uri, values); } break; default: throw new IllegalArgumentException("Error updating. Unrecognized uri: " + uri); } return result; } /** * Retrieves the object uuid either from the last path segment, the 'uuid' column * of the table, or throws an exception if a directory type Uri. * @param uri * @param resolver * @return */ public static synchronized String getUuid(Uri uri, ContentResolver resolver){ Log.i(TAG, "getUuid() " + uri); int descriptor = Uris.getTypeDescriptor(uri); Log.d(TAG,".... descriptor=" + descriptor); switch(descriptor){ case Uris.ITEM_UUID: return uri.getLastPathSegment(); case Uris.ITEM_ID: Cursor c = null; String uuid = null; try{ c = resolver.query(uri, new String[]{ BaseContract.UUID }, null,null,null); if (c!=null && c.moveToFirst()) uuid = c.getString(0); }catch (Exception e){ e.printStackTrace(); } finally { if(c != null) c.close(); } return uuid; default: throw new IllegalArgumentException("Invalid item uri: " + uri); } } /** * Retrieves the local row id either from the last path segment or the * {@link android.provider.BaseColumns#_ID _ID} column, of the table. * * @param uri The Uri to get the row id value for * @param resolver * @return The row id value * @throws IllegalArgumentException */ public static synchronized long getRowId(Uri uri, ContentResolver resolver){ switch(Uris.getTypeDescriptor(uri)){ case Uris.ITEM_ID: return Long.parseLong(uri.getLastPathSegment()); case Uris.ITEM_UUID: long id = -1; Cursor c = null; try{ c = resolver.query(uri, new String[]{ BaseContract._ID }, null,null,null); if (c.moveToFirst()) id = c.getInt(0); } finally { if(c != null) c.close(); } return id; default: throw new IllegalArgumentException("Invalid item uri"); } } }