/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.data; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import android.content.ContentValues; import android.os.Parcel; import android.os.Parcelable; import com.todoroo.astrid.data.Property.DoubleProperty; import com.todoroo.astrid.data.Property.IntegerProperty; import com.todoroo.astrid.data.Property.LongProperty; import com.todoroo.astrid.data.Property.PropertyVisitor; /** * <code>AbstractModel</code> represents a row in a database. * <p> * A single database can be represented by multiple <code>AbstractModel</code>s * corresponding to different queries that return a different set of columns. * Each model exposes a set of properties that it contains. * * @author Tim Su <tim@todoroo.com> * */ public abstract class AbstractModel implements Parcelable { // --- static variables private static final ContentValuesSavingVisitor saver = new ContentValuesSavingVisitor(); // --- constants /** id property common to all models */ protected static final String ID_PROPERTY_NAME = "_id"; //$NON-NLS-1$ /** id field common to all models */ public static final IntegerProperty ID_PROPERTY = new IntegerProperty(null, ID_PROPERTY_NAME); /** sentinel for objects without an id */ public static final long NO_ID = 0; // --- abstract methods /** Get the default values for this object */ abstract public ContentValues getDefaultValues(); // --- data store variables and management /* Data Source Ordering: * * In order to return the best data, we want to check first what the user * has explicitly set (setValues), then the values we have read out of * the database (values), then defaults (getDefaultValues) */ /** User set values */ protected ContentValues setValues = null; /** Values from database */ protected ContentValues values = null; /** Get database-read values for this object */ public ContentValues getDatabaseValues() { return values; } /** Get the user-set values for this object */ public ContentValues getSetValues() { return setValues; } /** Get a list of all field/value pairs merged across data sources */ public ContentValues getMergedValues() { ContentValues mergedValues = new ContentValues(); ContentValues defaultValues = getDefaultValues(); if(defaultValues != null) mergedValues.putAll(defaultValues); if(values != null) mergedValues.putAll(values); if(setValues != null) mergedValues.putAll(setValues); return mergedValues; } /** * Clear all data on this model */ public void clear() { values = null; setValues = null; } /** * Transfers all set values into values. This occurs when a task is * saved - future saves will not need to write all the data as before. */ public void markSaved() { if(values == null) values = setValues; else if(setValues != null) values.putAll(setValues); setValues = null; } /** * Use merged values to compare two models to each other. Must be of * exactly the same class. */ @Override public boolean equals(Object other) { if(other == null || other.getClass() != getClass()) return false; return getMergedValues().equals(((AbstractModel)other).getMergedValues()); } @Override public int hashCode() { return getMergedValues().hashCode() ^ getClass().hashCode(); } // --- data retrieval /** * Reads all properties from the supplied cursor and store */ protected synchronized void readPropertiesFromCursor(TodorooCursor<? extends AbstractModel> cursor) { if (values == null) values = new ContentValues(); // clears user-set values setValues = null; for (Property<?> property : cursor.getProperties()) { saver.save(property, values, cursor.get(property)); } } /** * Reads the given property. Make sure this model has this property! */ public synchronized <TYPE> TYPE getValue(Property<TYPE> property) { Object value; if(setValues != null && setValues.containsKey(property.name)) value = setValues.get(property.name); else if(values != null && values.containsKey(property.name) && (values.get(property.name) != null)) value = values.get(property.name); else if(getDefaultValues().containsKey(property.name)) value = getDefaultValues().get(property.name); else throw new UnsupportedOperationException( "Model Error: Did not read property " + property.name); //$NON-NLS-1$ // resolve properties that were retrieved with a different type than accessed if(value instanceof String && property instanceof LongProperty) return (TYPE) Long.valueOf((String)value); else if(value instanceof String && property instanceof IntegerProperty) return (TYPE) Integer.valueOf((String)value); if(value instanceof String && property instanceof DoubleProperty) return (TYPE) Double.valueOf((String)value); return (TYPE) value; } /** * Utility method to get the identifier of the model, if it exists. * * @return {@value #NO_ID} if this model was not added to the database */ abstract public long getId(); protected long getIdHelper(LongProperty id) { if(setValues != null && setValues.containsKey(id.name)) return setValues.getAsLong(id.name); else if(values != null && values.containsKey(id.name)) return values.getAsLong(id.name); else return NO_ID; } public void setId(long id) { if (setValues == null) setValues = new ContentValues(); if(id == NO_ID) setValues.remove(ID_PROPERTY_NAME); else setValues.put(ID_PROPERTY_NAME, id); } /** * @return true if this model has found Jesus (i.e. the database) */ public boolean isSaved() { return getId() != NO_ID; } /** * @param property * @return true if setValues or values contains this property */ public boolean containsValue(Property<?> property) { if(setValues != null && setValues.containsKey(property.name)) return true; if(values != null && values.containsKey(property.name)) return true; return false; } /** * @param property * @return true if setValues or values contains this property, and the value * stored is not null */ public boolean containsNonNullValue(Property<?> property) { if(setValues != null && setValues.containsKey(property.name)) return setValues.get(property.name) != null; if(values != null && values.containsKey(property.name)) return values.get(property.name) != null; return false; } // --- data storage /** * Check whether the user has changed this property value and it should be * stored for saving in the database */ protected synchronized <TYPE> boolean shouldSaveValue( Property<TYPE> property, TYPE newValue) { // we've already decided to save it, so overwrite old value if (setValues.containsKey(property.name)) return true; // values contains this key, we should check it out if(values != null && values.containsKey(property.name)) { TYPE value = getValue(property); if (value == null) { if (newValue == null) return false; } else if (value.equals(newValue)) return false; } // otherwise, good to save return true; } /** * Sets the given property. Make sure this model has this property! */ public synchronized <TYPE> void setValue(Property<TYPE> property, TYPE value) { if (setValues == null) setValues = new ContentValues(); if (!shouldSaveValue(property, value)) return; saver.save(property, setValues, value); } /** * Merges content values with those coming from another source */ public synchronized <TYPE> void mergeWith(ContentValues other) { if (setValues == null) setValues = other; else setValues.putAll(other); } /** * Clear the key for the given property * @param property */ public synchronized void clearValue(Property<?> property) { if(setValues != null && setValues.containsKey(property.name)) setValues.remove(property.name); else if(values != null && values.containsKey(property.name)) values.remove(property.name); else if(getDefaultValues().containsKey(property.name)) throw new IllegalArgumentException("Property has a default value"); //$NON-NLS-1$ } // --- property management /** * Looks inside the given class and finds all declared properties */ protected static Property<?>[] generateProperties(Class<? extends AbstractModel> cls) { ArrayList<Property<?>> properties = new ArrayList<Property<?>>(); if(cls.getSuperclass() != AbstractModel.class) properties.addAll(Arrays.asList(generateProperties( (Class<? extends AbstractModel>) cls.getSuperclass()))); // a property is public, static & extends Property for(Field field : cls.getFields()) { if((field.getModifiers() & Modifier.STATIC) == 0) continue; if(!Property.class.isAssignableFrom(field.getType())) continue; try { properties.add((Property<?>) field.get(null)); } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } return properties.toArray(new Property<?>[properties.size()]); } /** * Visitor that saves a value into a content values store * * @author Tim Su <tim@todoroo.com> * */ public static class ContentValuesSavingVisitor implements PropertyVisitor<Void, Object> { private ContentValues store; public synchronized void save(Property<?> property, ContentValues newStore, Object value) { this.store = newStore; property.accept(this, value); } public Void visitDouble(Property<Double> property, Object value) { store.put(property.name, (Double) value); return null; } public Void visitInteger(Property<Integer> property, Object value) { store.put(property.name, (Integer) value); return null; } public Void visitLong(Property<Long> property, Object value) { store.put(property.name, (Long) value); return null; } public Void visitString(Property<String> property, Object value) { store.put(property.name, (String) value); return null; } } // --- parcelable helpers /** * {@inheritDoc} */ public int describeContents() { return 0; } /** * {@inheritDoc} */ public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(setValues, 0); dest.writeParcelable(values, 0); } /** * In addition to overriding this class, model classes should create * a static final variable named "CREATOR" in order to satisfy the * requirements of the Parcelable interface. */ abstract protected Parcelable.Creator<? extends AbstractModel> getCreator(); /** * Parcelable creator helper */ protected static final class ModelCreator<TYPE extends AbstractModel> implements Parcelable.Creator<TYPE> { private final Class<TYPE> cls; public ModelCreator(Class<TYPE> cls) { super(); this.cls = cls; } /** * {@inheritDoc} */ public TYPE createFromParcel(Parcel source) { TYPE model; try { model = cls.newInstance(); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } model.setValues = source.readParcelable(ContentValues.class.getClassLoader()); model.values = source.readParcelable(ContentValues.class.getClassLoader()); return model; } /** * {@inheritDoc} */ public TYPE[] newArray(int size) { return (TYPE[]) Array.newInstance(cls, size); }; }; }