com.nonninz.robomodel.RoboModel.java Source code

Java tutorial

Introduction

Here is the source code for com.nonninz.robomodel.RoboModel.java

Source

/**
 * Copyright 2012 Francesco Donadon
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.nonninz.robomodel;

import static android.provider.BaseColumns._ID;
import static com.nonninz.robomodel.DatabaseManager.where;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nonninz.robomodel.annotations.Exclude;
import com.nonninz.robomodel.annotations.Save;
import com.nonninz.robomodel.exceptions.DatabaseNotUpToDateException;
import com.nonninz.robomodel.exceptions.InstanceNotFoundException;
import com.nonninz.robomodel.exceptions.JsonException;
import com.nonninz.robomodel.util.Ln;

/**
 * RoboModel:
 * 1. Provides ORM style methods to operate with Model instances
 * - save()
 * - delete()
 * - reload()
 * 
 */
@JsonAutoDetect(creatorVisibility = Visibility.NONE, fieldVisibility = Visibility.PUBLIC_ONLY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE, isGetterVisibility = Visibility.NONE)
public abstract class RoboModel {
    public static final long UNSAVED_MODEL_ID = -1;

    private String mTableName;

    protected long mId = UNSAVED_MODEL_ID;

    private final Class<? extends RoboModel> mClass = this.getClass();
    private Context mContext;
    private DatabaseManager mDatabaseManager;
    private final ObjectMapper mMapper = new ObjectMapper();

    protected void setContext(Context context) {
        mContext = context;
        mDatabaseManager = new DatabaseManager(context);
    }

    protected Context getContext() {
        return mContext;
    }

    public void delete() {
        if (!isSaved()) {
            throw new IllegalStateException("No record in database to delete");
        }

        mDatabaseManager.deleteRecord(getDatabaseName(), getTableName(), mId);
    }

    public String getDatabaseName() {
        return mDatabaseManager.getDatabaseName();
    }

    String getTableName() {
        if (mTableName == null) {
            mTableName = mClass.getSimpleName();
        }
        return mTableName;
    }

    public long getId() {
        return mId;
    }

    List<Field> getSavedFields() { //TODO: cache results
        final List<Field> savedFields = new ArrayList<Field>();

        final Field[] declaredFields = getClass().getDeclaredFields();
        boolean saved;
        for (final Field field : declaredFields) {

            saved = false;
            saved = saved || field.isAnnotationPresent(Save.class); // If @Save is present, save it
            saved = saved || Modifier.isPublic(field.getModifiers()); // If it is public, save it
            saved = saved && !Modifier.isStatic(field.getModifiers()); // If it is static, don't save it
            saved = saved && !field.isAnnotationPresent(Exclude.class); // If @Exclude, don't save it

            if (saved) {
                savedFields.add(field);
            }
        }

        return savedFields;
    }

    public boolean isSaved() {
        return mId != UNSAVED_MODEL_ID;
    }

    void load(long id) throws InstanceNotFoundException {
        if (id < 0) {
            throw new IllegalArgumentException("RoboModel id can not be negative.");
        }

        mId = id;
        reload();
    }

    private void loadField(Field field, Cursor query) throws DatabaseNotUpToDateException {
        final Class<?> type = field.getType();
        final boolean wasAccessible = field.isAccessible();
        final int columnIndex = query.getColumnIndex(field.getName());
        field.setAccessible(true);

        /*
         * TODO: There is the potential of a problem here:
         * What happens if the developer changes the type of a field between releases?
         *
         * If he saves first, then the column type will be changed (In the future).
         * If he loads first, we don't know if an Exception will be thrown if the
         * types are incompatible, because it's undocumented in the Cursor documentation.
         */

        try {
            if (type == String.class) {
                field.set(this, query.getString(columnIndex));
            } else if (type == Boolean.TYPE) {
                final boolean value = query.getInt(columnIndex) == 1 ? true : false;
                field.setBoolean(this, value);
            } else if (type == Byte.TYPE) {
                field.setByte(this, (byte) query.getShort(columnIndex));
            } else if (type == Double.TYPE) {
                field.setDouble(this, query.getDouble(columnIndex));
            } else if (type == Float.TYPE) {
                field.setFloat(this, query.getFloat(columnIndex));
            } else if (type == Integer.TYPE) {
                field.setInt(this, query.getInt(columnIndex));
            } else if (type == Long.TYPE) {
                field.setLong(this, query.getLong(columnIndex));
            } else if (type == Short.TYPE) {
                field.setShort(this, query.getShort(columnIndex));
            } else if (type.isEnum()) {
                final String string = query.getString(columnIndex);
                if (string != null && string.length() > 0) {
                    final Object[] constants = type.getEnumConstants();
                    final Method method = type.getMethod("valueOf", Class.class, String.class);
                    final Object value = method.invoke(constants[0], type, string);
                    field.set(this, value);
                }
            } else {
                // Try to de-json it (db column must be of type text)
                try {
                    final Object value = mMapper.readValue(query.getString(columnIndex), field.getType());
                    field.set(this, value);
                } catch (final Exception e) {
                    final String msg = String.format("Type %s is not supported for field %s", type,
                            field.getName());
                    Ln.w(e, msg);
                    throw new IllegalArgumentException(msg);
                }
            }
        } catch (final IllegalAccessException e) {
            final String msg = String.format("Field %s is not accessible", type, field.getName());
            throw new IllegalArgumentException(msg);
        } catch (final NoSuchMethodException e) {
            // Should not happen
            throw new RuntimeException(e);
        } catch (final InvocationTargetException e) {
            // Should not happen
            throw new RuntimeException(e);
        } catch (IllegalStateException e) {
            // This is when there is no column in db, but there is in the model
            throw new DatabaseNotUpToDateException(e);
        } finally {
            field.setAccessible(wasAccessible);
        }
    }

    @SuppressLint("DefaultLocale")
    public void reload() throws InstanceNotFoundException {
        if (!isSaved()) {
            throw new IllegalStateException("This instance has not yet been saved.");
        }

        // Retrieve current entry in the database
        SQLiteDatabase db = mDatabaseManager.openOrCreateDatabase(getDatabaseName());
        Cursor query;

        /*
         * Try to query the table. If the Table doesn't exist, fix the DB and re-run the query.
         */
        try {
            query = db.query(getTableName(), null, where(mId), null, null, null, null);
        } catch (final SQLiteException e) {
            mDatabaseManager.createOrPopulateTable(mTableName, getSavedFields(), db);
            query = db.query(getTableName(), null, where(mId), null, null, null, null);
        }

        if (query.moveToFirst()) {
            try {
                setFieldsWithQueryResult(query);
            } catch (DatabaseNotUpToDateException e) {
                Ln.w(e, "Updating table %s", mTableName);
                query.close();

                // Update table with new columns
                mDatabaseManager.createOrPopulateTable(mTableName, getSavedFields(), db);
                mDatabaseManager.closeDatabase();
                db = mDatabaseManager.openOrCreateDatabase(getDatabaseName());

                // Retry
                try {
                    query = db.query(getTableName(), null, where(mId), null, null, null, null);
                    query.moveToFirst();
                    setFieldsWithQueryResult(query);
                } catch (DatabaseNotUpToDateException ee) {
                    throw new RuntimeException("Could not repair database.", ee);
                }
            }
            query.close();
        } else {
            query.close();
            final String msg = String.format("No entry in database with id %d for model %s", getId(),
                    getTableName());
            throw new InstanceNotFoundException(msg);
        }
    }

    void loadRecord(int position) throws InstanceNotFoundException {
        // Retrieve current entry in the database
        SQLiteDatabase db = mDatabaseManager.openOrCreateDatabase(getDatabaseName());
        Cursor query;

        final String limit = String.format("%d,1", position);
        try {
            query = db.query(getTableName(), null, null, null, null, null, _ID, limit);
        } catch (final SQLiteException e) {
            mDatabaseManager.createOrPopulateTable(mTableName, getSavedFields(), db);
            query = db.query(getTableName(), null, null, null, null, null, _ID, limit);
        }

        if (query.moveToFirst()) {
            try {
                setFieldsWithQueryResult(query);
            } catch (DatabaseNotUpToDateException e) {
                Ln.w(e, "Updating table %s", mTableName);
                query.close();

                // Update table with new columns
                mDatabaseManager.createOrPopulateTable(mTableName, getSavedFields(), db);
                mDatabaseManager.closeDatabase();
                db = mDatabaseManager.openOrCreateDatabase(getDatabaseName());

                // Retry
                try {
                    query = db.query(getTableName(), null, null, null, null, null, _ID, limit);
                    query.moveToFirst();
                    setFieldsWithQueryResult(query);
                } catch (DatabaseNotUpToDateException ee) {
                    throw new RuntimeException("Could not repair database.", ee);
                }
            }
            // set ID
            mId = query.getLong(query.getColumnIndex(_ID));

            query.close();
        } else {
            query.close();
            final String msg = String.format("No entry in database in position %d for model %s", position,
                    getTableName());
            throw new InstanceNotFoundException(msg);
        }
    }

    public void save() {
        final SQLiteDatabase database = mDatabaseManager.openOrCreateDatabase(getDatabaseName());

        List<Field> fields = getSavedFields();
        final TypedContentValues cv = new TypedContentValues(fields.size());
        for (final Field field : fields) {
            saveField(field, cv);
        }

        // First try to save it. Then deal with errors (like table/field not existing);
        try {
            mId = mDatabaseManager.insertOrUpdate(getTableName(), cv, mId, database);
        } catch (final SQLiteException ex) {
            mDatabaseManager.createOrPopulateTable(getTableName(), fields, database);
            mId = mDatabaseManager.insertOrUpdate(getTableName(), cv, mId, database);
        }
    }

    void saveField(Field field, TypedContentValues cv) {
        final Class<?> type = field.getType();
        final boolean wasAccessible = field.isAccessible();
        field.setAccessible(true);

        try {
            if (type == String.class) {
                cv.put(field.getName(), (String) field.get(this));
            } else if (type == Boolean.TYPE) {
                cv.put(field.getName(), field.getBoolean(this));
            } else if (type == Byte.TYPE) {
                cv.put(field.getName(), field.getByte(this));
            } else if (type == Double.TYPE) {
                cv.put(field.getName(), field.getDouble(this));
            } else if (type == Float.TYPE) {
                cv.put(field.getName(), field.getFloat(this));
            } else if (type == Integer.TYPE) {
                cv.put(field.getName(), field.getInt(this));
            } else if (type == Long.TYPE) {
                cv.put(field.getName(), field.getLong(this));
            } else if (type == Short.TYPE) {
                cv.put(field.getName(), field.getShort(this));
            } else if (type.isEnum()) {
                final Object value = field.get(this);
                if (value != null) {
                    final Method method = type.getMethod("name");
                    final String str = (String) method.invoke(value);
                    cv.put(field.getName(), str);
                }
            } else {
                // Try to JSONify it (db column must be of type text)
                final String json = mMapper.writeValueAsString(field.get(this));
                cv.put(field.getName(), json);
            }
        } catch (final IllegalAccessException e) {
            final String msg = String.format("Field %s is not accessible", type, field.getName());
            throw new IllegalArgumentException(msg);
        } catch (final JsonProcessingException e) {
            Ln.w(e, "Error while dumping %s of type %s to Json", field.getName(), type);
            final String msg = String.format("Field %s is not accessible", type, field.getName());
            throw new IllegalArgumentException(msg);
        } catch (final NoSuchMethodException e) {
            // Should not happen
            throw new RuntimeException(e);
        } catch (final InvocationTargetException e) {
            // Should not happen
            throw new RuntimeException(e);
        } finally {
            field.setAccessible(wasAccessible);
        }
    }

    private void setFieldsWithQueryResult(Cursor query) throws DatabaseNotUpToDateException {
        // Iterate over the columns and auto-assign values on corresponding fields
        final List<Field> fields = getSavedFields();
        for (final Field field : fields) {
            loadField(field, query);
        }

        //        final String[] columns = query.getColumnNames();
        //
        //        for (final String column : columns) {
        //            // Skip id column
        //            if (column.equals(_ID)) {
        //                continue;
        //            }
        //
        //            final List<Field> fields = getSavedFields();
        //            for (final Field field : fields) {
        //                loadField(field, query);
        //            }
        //        }
    }

    @Override
    public String toString() {
        final List<Field> fields = getSavedFields();

        final StringBuilder b = new StringBuilder();
        b.append(getTableName() + " {id: " + getId() + ", ");
        String fieldName;
        for (final Field f : fields) {
            fieldName = f.getName();
            final boolean accessible = f.isAccessible();
            f.setAccessible(true);
            try {
                b.append(fieldName + ": " + f.get(this) + ", ");
            } catch (final IllegalAccessException e) {
                b.append(fieldName + ": (INACCESSIBLE), ");
            }
            f.setAccessible(accessible);
        }
        b.append("}");

        return b.toString();
    }

    public String toJson() {
        try {
            return mMapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            throw new JsonException(e);
        }
    }
}