package com.evancharlton.mileage.dao;
import com.evancharlton.mileage.exceptions.InvalidFieldException;
import com.evancharlton.mileage.provider.FillUpsProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.util.Log;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.List;
/**
* A base data access object (DAO). Exposes/provides the common functionality
* such as persisting objects.
*/
public abstract class Dao implements Cloneable {
private static final String TAG = "Dao";
public static final String _ID = BaseColumns._ID;
@Column(type = Column.LONG, name = BaseColumns._ID)
private long mId;
private Uri mUriBase = null;
private boolean mInMemoryDataChanged = false;
protected Dao(final ContentValues values) {
load(values);
}
public Dao(final Cursor cursor) {
load(cursor);
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
public void load(Cursor cursor) {
if (cursor.isBeforeFirst()) {
cursor.moveToFirst();
}
mId = cursor.getLong(cursor.getColumnIndex(_ID));
// automagically populate based on @Column annotation definitions
Field[] fields = getClass().getDeclaredFields();
for (Field field : fields) {
Column column = field.getAnnotation(Column.class);
if (column != null) {
int columnIndex = cursor.getColumnIndex(column.name());
Object value = null;
switch (column.type()) {
case Column.BOOLEAN:
value = cursor.getInt(columnIndex);
if (value == null) {
value = new Boolean(column.value() == 1);
} else {
value = ((Integer) value).intValue() == 1;
}
break;
case Column.DOUBLE:
value = cursor.getDouble(columnIndex);
if (value == null) {
value = new Double(column.value());
}
break;
case Column.INTEGER:
value = cursor.getInt(columnIndex);
if (value == null) {
value = new Integer(column.value());
}
break;
case Column.LONG:
value = cursor.getLong(columnIndex);
if (value == null) {
value = new Long(column.value());
}
break;
case Column.STRING:
value = cursor.getString(columnIndex);
if (value == null) {
value = "";
}
break;
case Column.TIMESTAMP:
Long ms = cursor.getLong(columnIndex);
if (ms != null) {
value = new Date(ms);
} else {
value = new Date(System.currentTimeMillis());
}
break;
}
if (value != null) {
try {
field.set(this, value);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Couldn't set value for " + field.getName(), e);
} catch (IllegalAccessException e) {
Log.e(TAG, "Couldn't access " + field.getName(), e);
}
}
}
}
}
// TODO(3.1) - Remove this code duplication.
public void load(ContentValues values) {
if (values == null) {
mId = -1;
return;
}
Long id = values.getAsLong(_ID);
if (id == null) {
mId = -1;
} else {
mId = id.longValue();
}
// automagically populate based on @Column annotation definitions
Field[] fields = getClass().getDeclaredFields();
for (Field field : fields) {
Column column = field.getAnnotation(Column.class);
if (column != null) {
Object value = null;
switch (column.type()) {
case Column.BOOLEAN:
value = values.getAsBoolean(column.name());
if (value == null) {
value = new Boolean(column.value() == 1);
}
break;
case Column.DOUBLE:
value = values.getAsDouble(column.name());
if (value == null) {
value = new Double(column.value());
}
break;
case Column.INTEGER:
value = values.getAsInteger(column.name());
if (value == null) {
value = new Integer(column.value());
}
break;
case Column.LONG:
value = values.getAsLong(column.name());
if (value == null) {
value = new Long(column.value());
}
break;
case Column.STRING:
value = values.getAsString(column.name());
if (value == null) {
value = "";
}
break;
case Column.TIMESTAMP:
Long ms = values.getAsLong(column.name());
if (ms != null) {
value = new Date(ms);
} else {
value = new Date(System.currentTimeMillis());
}
break;
}
if (value != null) {
try {
field.set(this, value);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Couldn't set value for " + field.getName(), e);
} catch (IllegalAccessException e) {
Log.e(TAG, "Couldn't access " + field.getName(), e);
}
}
}
}
}
/**
* Get the URI for this instance of a DAO.
*
* @return the URI for the DAO instance.
*/
public Uri getUri() {
if (mUriBase == null) {
DataObject annotation = getClass().getAnnotation(DataObject.class);
mUriBase = Uri.withAppendedPath(FillUpsProvider.BASE_URI, annotation.path());
}
if (isExistingObject()) {
return ContentUris.withAppendedId(mUriBase, getId());
}
return mUriBase;
}
/**
* Validate the data object (intended to be done before saving). If there is
* an invalid field value, throw an InvalidFieldException
*
* @return the ContentValues to be passed to persistent storage.
* @throws InvalidFieldException in the event of a validation error
*/
protected final void validate(ContentValues values) throws InvalidFieldException {
preValidate();
Field[] fields = getClass().getDeclaredFields();
for (Field field : fields) {
Validate validate = field.getAnnotation(Validate.class);
if (validate == null) {
continue;
}
int errorMessage = validate.value();
try {
Object value = field.get(this);
if (validate != null) {
if (errorMessage > 0) {
// see if it's null when it shouldn't be
if (value == null && field.getAnnotation(Nullable.class) != null) {
throw new InvalidFieldException(errorMessage);
}
// check strings
if (value instanceof String
&& field.getAnnotation(CanBeEmpty.class) == null) {
if (((String) value).length() == 0) {
throw new InvalidFieldException(errorMessage);
}
}
// check the numeric types
if (value instanceof Number) {
boolean checkPast = field.getAnnotation(Past.class) != null;
boolean checkPositive =
field.getAnnotation(Range.Positive.class) != null;
if (value instanceof Double) {
if (checkPositive && ((Double) value) <= 0D) {
throw new InvalidFieldException(errorMessage);
}
}
if (value instanceof Long) {
if (checkPositive && ((Long) value) <= 0L) {
throw new InvalidFieldException(errorMessage);
}
if (checkPast && ((Long) value) >= System.currentTimeMillis()) {
throw new InvalidFieldException(errorMessage);
}
}
if (value instanceof Integer) {
if (checkPositive && ((Integer) value) <= 0) {
throw new InvalidFieldException(errorMessage);
}
}
}
}
// we made it without any errors (or no validation needed)
Column column = field.getAnnotation(Column.class);
if (column != null) {
String data = null;
if (value instanceof Date) {
data = String.valueOf(((Date) value).getTime());
} else if (value instanceof Boolean) {
data = ((Boolean) value).booleanValue() ? "1" : "0";
} else {
data = String.valueOf(value);
}
values.put(column.name(), data);
}
}
} catch (IllegalArgumentException e) {
Log.e(TAG, e.getMessage(), e);
} catch (IllegalAccessException e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
protected void preValidate() {
}
public boolean save(Context context) throws InvalidFieldException {
ContentValues values = new ContentValues();
validate(values);
if (isExistingObject()) {
// update
values.put(_ID, mId);
context.getContentResolver().update(getUri(), values, null, null);
} else {
// insert
Uri uri = context.getContentResolver().insert(getUri(), values);
List<String> segments = uri.getPathSegments();
String id = segments.get(segments.size() - 1);
mId = Long.parseLong(id);
}
return true;
}
public boolean saveIfChanged(Context context) throws InvalidFieldException {
if (mInMemoryDataChanged) {
return save(context);
}
return false;
}
public boolean delete(Context context) {
return context.getContentResolver().delete(getUri(), null, null) > 0;
}
public final boolean isExistingObject() {
return mId > 0;
}
public final long getId() {
return mId;
}
public final void setId(long id) {
mId = id;
}
protected void setInMemoryDataChanged() {
mInMemoryDataChanged = true;
}
protected long getLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
protected double getDouble(Cursor cursor, String columnName) {
return cursor.getDouble(cursor.getColumnIndex(columnName));
}
protected String getString(Cursor cursor, String columnName) {
return cursor.getString(cursor.getColumnIndex(columnName));
}
protected boolean getBoolean(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName)) == 1;
}
protected int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
protected int getInt(ContentValues values, String key, int defaultValue) {
Integer value = values.getAsInteger(key);
if (value != null) {
return value.intValue();
}
return defaultValue;
}
protected String getString(ContentValues values, String key, String defaultValue) {
String value = values.getAsString(key);
if (value != null) {
return value;
}
return defaultValue;
}
protected double getDouble(ContentValues values, String key, double defaultValue) {
Double value = values.getAsDouble(key);
if (value != null) {
return value.doubleValue();
}
return defaultValue;
}
protected boolean getBoolean(ContentValues values, String key, boolean defaultValue) {
Boolean value = values.getAsBoolean(key);
if (value != null) {
return value.booleanValue();
}
return defaultValue;
}
protected long getLong(ContentValues values, String key, long defaultValue) {
Long value = values.getAsLong(key);
if (value != null) {
return value.longValue();
}
return defaultValue;
}
// TODO: make this a series of annotations instead?
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Column {
public static final int STRING = 0;
public static final int INTEGER = 1;
public static final int DOUBLE = 2;
public static final int BOOLEAN = 3;
public static final int TIMESTAMP = 4;
public static final int LONG = 5;
int value() default 0;
int type();
String name();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DataObject {
String path();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Validate {
int value() default 0;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Nullable {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CanBeEmpty {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Past {
}
public static class Range {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Positive {
}
}
}