diff --git a/README.md b/README.md index d17661f..98f87dc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,213 @@ -sprinkles +Sprinkles [![Icon](https://github.com/emilsjolander/sprinkles/raw/master/sprinkles.png)] ========= +Sprinkles is a boiler-plate-reduction-library for dealing with databases in android applications. Some would call is a kind of ORM but i don't see it that way. Sprinkles does lets SQL do what it is good at, making complex queries. SQL is however a mess (in my opinion) when is comes to everything else. This is why sprinkles helps you with things such as inserting, updated and destroying models, spinkles will also help you with the tedious task of unpacking a cursor into a model. Sprinkles actively supports version 2.3 of android and above but it should work on alder versions as well. + +Download +-------- +I prefer cloning my libraries and adding them as a dependency manually. This way i can easily fix a bug in the library as part of my workflow and commit it upstream (pleas do!). + +However i know a lot of people like using gradle/maven so here is the dependency to add to your `build.gradle` or `pom.xml`. + +```Groovy +__TODO__ +``` +```xml +__TODO__ +``` + +Getting started +--------------- +When you have added the library to your project add a model class to it. I will demonstrate this with a `Note.java` class. I have omitted import statements to keep it brief. +```java +@Table("Notes") +public class Note extends Model { + + @AutoIncrementPrimaryKey + @Column("id") + private long id; + + @Column("title") + public String title; + + @Column("body") + public String body; + + public long getId() { + return id; + } + +} +``` +Ok, a lot of important stuff in this short class. First of all. A model must subclass `se.emilsjolander.sprinkles.Model` and it also must have a `@Table` annotations specifying the table name the model corresponds to. After the class declaration we have declared three members: `id`, `title` and `body`. Notice how all of them have a `@Column` annotation to mark that they are not only a member of this class but also a column of the table that this class represents. We have one last annotation in the above example: @AutoIncrementPrimaryKey, this annotation tells sprinkles that the field is both an autoincrement field and a primary key field. A field with this annotation will automatically be set upon the creation of its corresponding row in the table. + +Before using this class you must migrate it into the database. I recomend doing thing in the `onCreate()` method of an `Application` subclass like this: +```java +public class MyApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + + Sprinkles sprinkles = Sprinkles.getInstance(getApplicationContext()); + + Migration initialMigration = new Migration(); + initialMigration.createTable(Note.class); + sprinkles.addMigration(initialMigration); + } + +} +``` + +Now you can happilty create new instances of this class and save it to the database like so: +```java +public void saveStuff() { + Note n = new Note(); + n.title = "Sprinkles is awesome!"; + n.body = "yup, sure is!; + n.save(); // when this call finishes n.getId() will return a valid id +} +``` + +You can also query for this note like this: +```java +public void queryStuff() { + Note n = Query.one("select * from Notes where title=?", "Sprinkles is awesome!").get(); +} +``` + +There is a lot more you can do with sprinkles so please read the next section which covers the whole api! + +API +--- +###Annotations +- `@Table`: Used to associate a model class with a sql table. +- `@AutoIncrementPrimaryKey`: Used to mark a field a an autoincrementing primary key. The field must be an `int` or a `long` and cannot be in the same class as any other primary key. +- `@Column`: Used to associate a class field with a sql column. +- `@PrimaryKey`: Used to mark a field as a primary key. Multiple primary keys in a class are allowed and will result in a composite primary key. +- `@ForeignKey`: Used to mark a field as a foreign key. The argument given to this annotation should be foreignKeyTable(foreignKeyColumn). +- `@CascadeDelete`: Used to mark a field also marked as a foreign key as a cascade deleting field. + +###Saving +The save method is both an insert and a update method, the correct thing will be done depending on if the model exists in the database or not. The two first methods below and syncronous, the second is for using together with a transaction (more on the later). There are also two asyncronous methods, one with a callback and one without. The syncronous methods will return a boolean indicating if the model was saved or not, The asyncronous method with a callback will just not invoke the callback if saving failed. +```java +boolean save(); +boolean save(Transaction t); +void saveAsync(); +void saveAsync(OnSavedCallback callback); +``` + +All the save methods use this method to check if a model exists in the database. You are free to use it as well. +```java +boolean exists(); +``` + +###Deleting +Similar to saving there are four methods that let you delete a model. These work in the same way as save but will not return a boolean indicating the result. +```java +void delete(); +void delete(Transaction t); +void deleteAsync(); +void deleteAsync(OnDeletedCallback callback); +``` + +###Querying +Start a query with on of the following static methods: +```java +Query.One(Class clazz, String sql, Object[] args); +Query.Many(Class clazz, String sql, Object[] args); +``` +Notice that unlike android built in query methods you can send in an array of objects instead of an array of strings. + +Once the query has been started you can get the result with three different methods: +```java +get(); +getAsync(LoaderManager lm, OnQueryResultHandler handler); +getAsyncWithUpdates(LoaderManager lm, OnQueryResultHandler handler, Class... dependencies); +``` + +`get()` return either the model or a list of the model represented by the `Class` you sent in as the first argument to the query method. `getAsync()` is the same only that the result is delivered on a callback function after the executeing `get()` on another thread. `getAsyncWithUpdates()` is the same as `getAsync()` only that it delivers updated results once the backing model of the query is updated. Both of the async methods use loaders and therefore need a `LoaderManager` instance. `getAsyncWithUpdates()` takes in an optional array of classes, this is used when the query relies on more models than the one you are querying for and you want the query to updated when those models change as well. + +###Transactions +Both `save()` and `delete()` methods exists which take in a `Transaction`. Here is a quick example on how t use them. If any exception is thrown while saving a note or if any note fails to save the transaction will be rolled back. +```java +public void doTransaction(List notes) { + Transaction t = new Transaction(); + try { + for (Note n : notes) { + if (n.save(t)) { + return; + } + } + t.setSuccessful(true); + } finally { + t.finish(); + } +} +``` + +###Callbacks +Each model subclass can override a couple of callbacks. + +Use the following callback to ensure that your model is not saved in an invalid state. +```java +@Override +public boolean isValid() { + // check model validity +} +``` + +Use the following callback to update a variable before the model is created +```java +@Override +protected void beforeCreate() { + mCreatedAt = System.currentTimeMillis(); +} +``` + +Use the following callback to update a variable before the model is saved. This is called directly before `beforeCreate()` if the model is saved for the first time. +```java +@Override +protected void beforeSave() { + mUpdatedAt = System.currentTimeMillis(); +} +``` + +User the following callback to clean up things related to the model but not stored in the databse. Perhaps a file on the internal storage? +```java +@Override +protected void afterDelete() { + // clean up some things? +} +``` + +###Migrations +Migrations are the way you add things to your database. I suggest putting all your migrations in the `onCreate()` method of a `Application` subclass. Here is a quick example of how that would look: +```java +public class MyApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + + Sprinkles sprinkles = Sprinkles.getInstance(getApplicationContext()); + + Migration initialMigration = new Migration(); + initialMigration.createTable(Note.class); + sprinkles.addMigration(initialMigration); + } + +} +``` +The above example uses the `createTable()` method on a migration. Here is a full list of the methods it supports: +```java +void createTable(Class clazz); +void dropTable(Class clazz); +void renameTable(String from, String to); +void addColumn(Class clazz, String columnName); +void addRawStatement(String statement); +``` +Any number of calls to any of the above migrations are allowed, if for example `createTable()` is called twice than two tables will be created once that migration has been added. Remember to never edit a migration, always create a new migration (this only applies to production version of the app of course). + +###Relationships +Sprinkles does nothing to handle relationships for you, this is by design. You will have to use the regular ways to handle relationships in sql. Sprinkles gives you all the tools needed for this and it works very well. + diff --git a/library/.settings/org.eclipse.jdt.core.prefs b/library/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..b080d2d --- /dev/null +++ b/library/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/library/AndroidManifest.xml b/library/AndroidManifest.xml new file mode 100644 index 0000000..2ae2495 --- /dev/null +++ b/library/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/library/libs/android-support-v4.jar b/library/libs/android-support-v4.jar new file mode 100644 index 0000000..cf12d28 Binary files /dev/null and b/library/libs/android-support-v4.jar differ diff --git a/library/lint.xml b/library/lint.xml new file mode 100644 index 0000000..ee0eead --- /dev/null +++ b/library/lint.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/library/proguard-project.txt b/library/proguard-project.txt new file mode 100644 index 0000000..f2fe155 --- /dev/null +++ b/library/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/library/project.properties b/library/project.properties new file mode 100644 index 0000000..1b8c5a3 --- /dev/null +++ b/library/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-18 +android.library=true diff --git a/library/res/drawable-hdpi/ic_launcher.png b/library/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..96a442e Binary files /dev/null and b/library/res/drawable-hdpi/ic_launcher.png differ diff --git a/library/res/drawable-xhdpi/ic_launcher.png b/library/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..71c6d76 Binary files /dev/null and b/library/res/drawable-xhdpi/ic_launcher.png differ diff --git a/library/res/values/strings.xml b/library/res/values/strings.xml new file mode 100644 index 0000000..6d7e60f --- /dev/null +++ b/library/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Cobra + + diff --git a/library/res/values/styles.xml b/library/res/values/styles.xml new file mode 100644 index 0000000..6ce89c7 --- /dev/null +++ b/library/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/library/src/se/emilsjolander/sprinkles/ColumnField.java b/library/src/se/emilsjolander/sprinkles/ColumnField.java new file mode 100644 index 0000000..de5c9a4 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/ColumnField.java @@ -0,0 +1,26 @@ +package se.emilsjolander.sprinkles; + +import java.lang.reflect.Field; + +class ColumnField { + + String name; + String type; + String foreignKey; + + boolean isPrimaryKey; + boolean isForeignKey; + boolean isAutoIncrementPrimaryKey; + boolean isCascadeDelete; + + public Field field; + + @Override + public boolean equals(Object o) { + if (o instanceof ColumnField) { + return ((ColumnField) o).name.equals(name); + } + return false; + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/CursorLoader.java b/library/src/se/emilsjolander/sprinkles/CursorLoader.java new file mode 100644 index 0000000..46cb397 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/CursorLoader.java @@ -0,0 +1,122 @@ +package se.emilsjolander.sprinkles; + +import java.util.List; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +class CursorLoader extends AsyncTaskLoader { + + private final ForceLoadContentObserver mObserver; + + private String mSql; + private List> mDependencies; + private Cursor mCursor; + + public CursorLoader(Context context, String sql, + List> dependencies) { + super(context); + mObserver = new ForceLoadContentObserver(); + mSql = sql; + mDependencies = dependencies; + } + + /* Runs on a worker thread */ + @Override + public Cursor loadInBackground() { + final SQLiteDatabase db = DbOpenHelper.getInstance(); + Cursor cursor = db.rawQuery(mSql, null); + + if (cursor != null) { + // Ensure the cursor window is filled + cursor.getCount(); + + if (mDependencies != null) { + cursor.registerContentObserver(mObserver); + for (Class dependency : mDependencies) { + getContext().getContentResolver().registerContentObserver( + Utils.getNotificationUri(dependency), false, mObserver); + } + } + } + return cursor; + } + + /* Runs on the UI thread */ + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + // An async query came in while the loader is stopped + if (cursor != null) { + cursor.close(); + } + return; + } + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (isStarted()) { + super.deliverResult(cursor); + } + + if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { + oldCursor.close(); + } + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is + * ready the callbacks will be called on the UI thread. If a previous load + * has been completed and is still valid the result may be passed to the + * callbacks immediately. + * + * Must be called from the UI thread + */ + @Override + protected void onStartLoading() { + if (mCursor != null) { + deliverResult(mCursor); + } + if (takeContentChanged() || mCursor == null) { + forceLoad(); + } + } + + /** + * Must be called from the UI thread + */ + @Override + protected void onStopLoading() { + // Attempt to cancel the current load task if possible. + cancelLoad(); + } + + @Override + public void onCanceled(Cursor cursor) { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + + @Override + protected void onAbandon() { + super.onAbandon(); + getContext().getContentResolver().unregisterContentObserver(mObserver); + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + + if (mCursor != null && !mCursor.isClosed()) { + mCursor.close(); + } + mCursor = null; + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/DbOpenHelper.java b/library/src/se/emilsjolander/sprinkles/DbOpenHelper.java new file mode 100644 index 0000000..d747c5d --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/DbOpenHelper.java @@ -0,0 +1,43 @@ +package se.emilsjolander.sprinkles; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +class DbOpenHelper extends SQLiteOpenHelper { + + private DbOpenHelper(Context context) { + super(context, "sprinkles.db", null, + Sprinkles.sInstance.mMigrations.size()); + } + + public void onCreate(SQLiteDatabase db) { + executeMigrations(db, 0, Sprinkles.sInstance.mMigrations.size()); + } + + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + executeMigrations(db, oldVersion, newVersion); + } + + @Override + public void onOpen(SQLiteDatabase db) { + super.onOpen(db); + db.execSQL("PRAGMA foreign_keys=ON;"); + } + + private void executeMigrations(SQLiteDatabase db, int oldVersion, int newVersion) { + for (int i = oldVersion; i < newVersion; i++) { + Sprinkles.sInstance.mMigrations.get(i).execute(db); + } + } + + private static SQLiteDatabase sInstance; + + static synchronized SQLiteDatabase getInstance() { + if (sInstance == null) { + sInstance = new DbOpenHelper(Sprinkles.sInstance.mContext).getWritableDatabase(); + } + return sInstance; + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/ManyQuery.java b/library/src/se/emilsjolander/sprinkles/ManyQuery.java new file mode 100644 index 0000000..f27ac75 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/ManyQuery.java @@ -0,0 +1,136 @@ +package se.emilsjolander.sprinkles; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import se.emilsjolander.sprinkles.Query.OnQueryResultHandler; +import android.app.LoaderManager; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.Loader; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.os.Bundle; + + +public final class ManyQuery { + + Class resultClass; + String sqlQuery; + + ManyQuery() { + } + + public List get() { + final SQLiteDatabase db = DbOpenHelper.getInstance(); + final Cursor c = db.rawQuery(sqlQuery, null); + + List result = new ArrayList(); + while (c.moveToNext()) { + result.add(Utils.getModelFromCursor(resultClass, c)); + } + + c.close(); + return result; + } + + public void getAsync(LoaderManager lm, OnQueryResultHandler> handler) { + final int loaderId = sqlQuery.hashCode(); + lm.initLoader(loaderId, null, + getLoaderCallbacks(sqlQuery, resultClass, handler, false, null)); + } + + public void getAsyncWithUpdates(LoaderManager lm, + OnQueryResultHandler> handler, Class... respondsToUpdatedOf) { + + // list of classes which when updated will trigger a reload of the dataset + final List> listOfDependencies = Arrays.asList(respondsToUpdatedOf); + listOfDependencies.add(resultClass); + + final int loaderId = sqlQuery.hashCode(); + lm.initLoader(loaderId, null, + getLoaderCallbacks(sqlQuery, resultClass, handler, true, listOfDependencies)); + } + + private LoaderCallbacks getLoaderCallbacks(final String sqlQuery, + final Class resultClass, + final OnQueryResultHandler> handler, + final boolean getUpdates, final List> respondsToUpdatedOf) { + return new LoaderCallbacks() { + + @Override + public void onLoaderReset(Loader arg0) { + handler.onResult(new ArrayList()); + } + + @Override + public void onLoadFinished(Loader loader, Cursor c) { + List result = new ArrayList(); + while (c.moveToNext()) { + result.add(Utils.getModelFromCursor(resultClass, c)); + } + handler.onResult(result); + + if (!getUpdates) { + loader.abandon(); + } + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new CursorLoader(Sprinkles.sInstance.mContext, sqlQuery, + respondsToUpdatedOf); + } + }; + } + + public void getAsync(android.support.v4.app.LoaderManager lm, OnQueryResultHandler> handler) { + final int loaderId = sqlQuery.hashCode(); + lm.initLoader(loaderId, null, + getSupportLoaderCallbacks(sqlQuery, resultClass, handler, false, null)); + } + + public void getAsyncWithUpdates(android.support.v4.app.LoaderManager lm, + OnQueryResultHandler> handler, Class... respondsToUpdatedOf) { + + // list of classes which when updated will trigger a reload of the dataset + final List> listOfDependencies = Arrays.asList(respondsToUpdatedOf); + listOfDependencies.add(resultClass); + + final int loaderId = sqlQuery.hashCode(); + lm.initLoader(loaderId, null, + getSupportLoaderCallbacks(sqlQuery, resultClass, handler, true, listOfDependencies)); + } + + private android.support.v4.app.LoaderManager.LoaderCallbacks getSupportLoaderCallbacks(final String sqlQuery, + final Class resultClass, final OnQueryResultHandler> handler, + final boolean getUpdates, final List> respondsToUpdatedOf) { + return new android.support.v4.app.LoaderManager.LoaderCallbacks() { + + @Override + public void onLoaderReset(android.support.v4.content.Loader arg0) { + handler.onResult(new ArrayList()); + } + + @Override + public void onLoadFinished(android.support.v4.content.Loader loader, Cursor c) { + List result = new ArrayList(); + while (c.moveToNext()) { + result.add(Utils.getModelFromCursor(resultClass, c)); + } + handler.onResult(result); + + if (!getUpdates) { + loader.abandon(); + } + } + + @Override + public android.support.v4.content.Loader onCreateLoader(int id, Bundle args) { + return new SupportCursorLoader(Sprinkles.sInstance.mContext, sqlQuery, + respondsToUpdatedOf); + } + }; + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/Migration.java b/library/src/se/emilsjolander/sprinkles/Migration.java new file mode 100644 index 0000000..98ffffb --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/Migration.java @@ -0,0 +1,130 @@ +package se.emilsjolander.sprinkles; + +import java.util.ArrayList; +import java.util.List; + +import se.emilsjolander.sprinkles.exceptions.NoSuchColumnFoundException; +import android.database.sqlite.SQLiteDatabase; + +public class Migration { + + private List mStatements = new ArrayList(); + + void execute(SQLiteDatabase db) { + for (String sql : mStatements) { + db.execSQL(sql); + } + } + + public void createTable(Class clazz) { + final String tableName = Utils.getTableName(clazz); + final StringBuilder createStatement = new StringBuilder(); + + createStatement.append("CREATE TABLE "); + createStatement.append(tableName); + createStatement.append("("); + + final List columns = Utils.getColumns(clazz); + final List primaryColumns = new ArrayList(); + final List foreignColumns = new ArrayList(); + for (int i = 0; i < columns.size(); i++) { + final ColumnField column = columns.get(i); + createStatement.append(column.name + " "); + createStatement.append(column.type); + + if (column.isAutoIncrementPrimaryKey) { + createStatement.append(" PRIMARY KEY AUTOINCREMENT"); + }else { + if (column.isPrimaryKey) { + primaryColumns.add(column); + } + + if (column.isForeignKey) { + foreignColumns.add(column); + } + } + + // add a comma separator between columns if it is not the last + // column + if (i < columns.size() - 1 || !primaryColumns.isEmpty() + || !foreignColumns.isEmpty()) { + createStatement.append(", "); + } + } + + if (!primaryColumns.isEmpty()) { + createStatement.append("PRIMARY KEY("); + + for (int i = 0; i < primaryColumns.size(); i++) { + final ColumnField column = primaryColumns.get(i); + createStatement.append(column.name); + + // add a comma separator between keys if it is not the last + // primary key + if (i < primaryColumns.size() - 1) { + createStatement.append(", "); + } + } + + createStatement.append(")"); + + // add a comma separator if there are foreign keys to add + if (!foreignColumns.isEmpty()) { + createStatement.append(", "); + } + } + + for (int i = 0; i < foreignColumns.size(); i++) { + final ColumnField column = foreignColumns.get(i); + createStatement.append("FOREIGN KEY("); + createStatement.append(column.name); + createStatement.append(") REFERENCES "); + createStatement.append(column.foreignKey); + if (column.isCascadeDelete) { + createStatement.append(" ON DELETE CASCADE"); + } + + // add a comma separator if there are still foreign keys to add + if (i < foreignColumns.size() - 1) { + createStatement.append(", "); + } + } + + createStatement.append(");"); + + mStatements.add(createStatement.toString()); + } + + public void dropTable(Class clazz) { + final String tableName = Utils.getTableName(clazz); + mStatements.add(String.format("DROP TABLE IF EXISTS %s;", tableName)); + } + + public void renameTable(String from, String to) { + mStatements.add(String.format("ALTER TABLE %s RENAME TO %s;", from, to)); + } + + public void addColumn(Class clazz, String columnName) { + final String tableName = Utils.getTableName(clazz); + ColumnField column = null; + + List fields = Utils.getColumns(clazz); + for (ColumnField field : fields) { + if (field.name.equals(columnName)) { + column = field; + } + } + + if (column == null) { + throw new NoSuchColumnFoundException(columnName); + } + + mStatements.add(String.format("ALTER TABLE %s ADD COLUMN %s %s;", + tableName, column, column.type)); + } + + public void addRawStatement(String statement) { + mStatements.add(statement); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/Model.java b/library/src/se/emilsjolander/sprinkles/Model.java new file mode 100644 index 0000000..098c484 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/Model.java @@ -0,0 +1,149 @@ +package se.emilsjolander.sprinkles; + +import java.util.List; + +import se.emilsjolander.sprinkles.Transaction.OnTransactionCommittedListener; +import android.os.AsyncTask; + +public abstract class Model { + + public interface OnSavedCallback { + void onSaved(); + } + + public interface OnDeletedCallback { + void onDeleted(); + } + + public boolean isValid() { + // optionally implemented by subclass + return true; + } + + protected void beforeCreate() { + // optionally implemented by subclass + } + + protected void beforeSave() { + // optionally implemented by subclass + } + + protected void afterDelete() { + // optionally implemented by subclass + } + + final public boolean exists() { + final Model m = Query.one( + getClass(), + String.format("SELECT * FROM %s WHERE %s LIMIT 1", + Utils.getTableName(getClass()), + Utils.getWhereStatement(this))).get(); + return m != null; + } + + final public boolean save() { + Transaction t = new Transaction(); + try { + t.setSuccessful(save(t)); + } finally { + t.finish(); + } + return t.isSuccessful(); + } + + final public boolean save(Transaction t) { + if (!isValid()) { + return false; + } + + beforeSave(); + if (exists()) { + t.update(Utils.getTableName(getClass()), + Utils.getContentValues(this), Utils.getWhereStatement(this)); + } else { + beforeCreate(); + long id = t.insert(Utils.getTableName(getClass()), + Utils.getContentValues(this)); + + // set the @AutoIncrement column if one exists + final List columns = Utils.getColumns(getClass()); + for (ColumnField column : columns) { + if (column.isAutoIncrementPrimaryKey) { + column.field.setAccessible(true); + try { + column.field.set(this, id); + } catch (Exception e) { + throw new RuntimeException(e); + } + break; + } + } + } + + t.addOnTransactionCommittedListener(new OnTransactionCommittedListener() { + + @Override + public void onTransactionCommitted() { + Sprinkles.sInstance.mContext.getContentResolver().notifyChange( + Utils.getNotificationUri(Model.this.getClass()), null); + } + }); + + return true; + } + + final public void saveAsync() { + saveAsync(null); + } + + final public void saveAsync(final OnSavedCallback callback) { + new AsyncTask() { + + protected Boolean doInBackground(Model... params) { + return params[0].save(); + } + + protected void onPostExecute(Boolean result) { + if (result) { + callback.onSaved(); + } + } + + }.execute(this); + } + + final public void delete() { + Transaction t = new Transaction(); + try { + delete(t); + t.setSuccessful(true); + } finally { + t.finish(); + } + } + + final public void delete(Transaction t) { + t.delete(Utils.getTableName(getClass()), Utils.getWhereStatement(this)); + afterDelete(); + } + + final public void deleteAsync() { + deleteAsync(null); + } + + final public void deleteAsync(final OnDeletedCallback callback) { + new AsyncTask() { + + protected Void doInBackground(Model... params) { + params[0].delete(); + return null; + } + + protected void onPostExecute(Void result) { + callback.onDeleted(); + } + + }.execute(this); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/OneQuery.java b/library/src/se/emilsjolander/sprinkles/OneQuery.java new file mode 100644 index 0000000..de52bef --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/OneQuery.java @@ -0,0 +1,133 @@ +package se.emilsjolander.sprinkles; + +import java.util.Arrays; +import java.util.List; + +import se.emilsjolander.sprinkles.Query.OnQueryResultHandler; +import android.app.LoaderManager; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.Loader; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.os.Bundle; + +public final class OneQuery { + + Class resultClass; + String sqlQuery; + + OneQuery() { + } + + public T get() { + final SQLiteDatabase db = DbOpenHelper.getInstance(); + final Cursor c = db.rawQuery(sqlQuery, null); + + T result = null; + if (c.moveToFirst()) { + result = Utils.getModelFromCursor(resultClass, c); + } + + c.close(); + return result; + } + + public void getAsync(LoaderManager lm, OnQueryResultHandler handler) { + final int loaderId = sqlQuery.hashCode(); + lm.initLoader(loaderId, null, + getLoaderCallbacks(sqlQuery, resultClass, handler, false, null)); + } + + public void getAsyncWithUpdates(LoaderManager lm, + OnQueryResultHandler handler, Class... respondsToUpdatedOf) { + + // list of classes which when updated will trigger a reload of the dataset + final List> listOfDependencies = Arrays.asList(respondsToUpdatedOf); + listOfDependencies.add(resultClass); + + final int loaderId = sqlQuery.hashCode(); + lm.initLoader(loaderId, null, + getLoaderCallbacks(sqlQuery, resultClass, handler, true, listOfDependencies)); + } + + private LoaderCallbacks getLoaderCallbacks(final String sqlQuery, + final Class resultClass, final OnQueryResultHandler handler, + final boolean getUpdates, final List> respondsToUpdatedOf) { + return new LoaderCallbacks() { + + @Override + public void onLoaderReset(Loader arg0) { + handler.onResult(null); + } + + @Override + public void onLoadFinished(Loader loader, Cursor c) { + T result = null; + if (c.moveToFirst()) { + result = Utils.getModelFromCursor(resultClass, c); + } + handler.onResult(result); + + if (!getUpdates) { + loader.abandon(); + } + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new CursorLoader(Sprinkles.sInstance.mContext, sqlQuery, + respondsToUpdatedOf); + } + }; + } + + public void getAsync(android.support.v4.app.LoaderManager lm, OnQueryResultHandler handler) { + final int loaderId = sqlQuery.hashCode(); + lm.initLoader(loaderId, null, + getSupportLoaderCallbacks(sqlQuery, resultClass, handler, false, null)); + } + + public void getAsyncWithUpdates(android.support.v4.app.LoaderManager lm, + OnQueryResultHandler handler, Class... respondsToUpdatedOf) { + + // list of classes which when updated will trigger a reload of the dataset + final List> listOfDependencies = Arrays.asList(respondsToUpdatedOf); + listOfDependencies.add(resultClass); + + final int loaderId = sqlQuery.hashCode(); + lm.initLoader(loaderId, null, + getSupportLoaderCallbacks(sqlQuery, resultClass, handler, true, listOfDependencies)); + } + + private android.support.v4.app.LoaderManager.LoaderCallbacks getSupportLoaderCallbacks(final String sqlQuery, + final Class resultClass, final OnQueryResultHandler handler, + final boolean getUpdates, final List> respondsToUpdatedOf) { + return new android.support.v4.app.LoaderManager.LoaderCallbacks() { + + @Override + public void onLoaderReset(android.support.v4.content.Loader arg0) { + handler.onResult(null); + } + + @Override + public void onLoadFinished(android.support.v4.content.Loader loader, Cursor c) { + T result = null; + if (c.moveToFirst()) { + result = Utils.getModelFromCursor(resultClass, c); + } + handler.onResult(result); + + if (!getUpdates) { + loader.abandon(); + } + } + + @Override + public android.support.v4.content.Loader onCreateLoader(int id, Bundle args) { + return new SupportCursorLoader(Sprinkles.sInstance.mContext, sqlQuery, + respondsToUpdatedOf); + } + }; + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/Query.java b/library/src/se/emilsjolander/sprinkles/Query.java new file mode 100644 index 0000000..a92e389 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/Query.java @@ -0,0 +1,29 @@ +package se.emilsjolander.sprinkles; + + +public final class Query { + + public interface OnQueryResultHandler { + void onResult(T result); + } + + private Query() { + } + + public static OneQuery one(Class clazz, String sql, + Object... sqlArgs) { + final OneQuery query = new OneQuery(); + query.resultClass = clazz; + query.sqlQuery = Utils.insertSqlArgs(sql, sqlArgs); + return query; + } + + public static ManyQuery many(Class clazz, String sql, + Object... sqlArgs) { + final ManyQuery query = new ManyQuery(); + query.resultClass = clazz; + query.sqlQuery = Utils.insertSqlArgs(sql, sqlArgs); + return query; + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/Sprinkles.java b/library/src/se/emilsjolander/sprinkles/Sprinkles.java new file mode 100644 index 0000000..683169f --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/Sprinkles.java @@ -0,0 +1,30 @@ +package se.emilsjolander.sprinkles; + +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; + +public class Sprinkles { + + static Sprinkles sInstance; + Context mContext; + List mMigrations = new ArrayList(); + + private Sprinkles() { + // do nothing + } + + public static Sprinkles getInstance(Context context) { + if (sInstance == null) { + sInstance = new Sprinkles(); + } + sInstance.mContext = context.getApplicationContext(); + return sInstance; + } + + public void addMigration(Migration migration) { + mMigrations.add(migration); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/SupportCursorLoader.java b/library/src/se/emilsjolander/sprinkles/SupportCursorLoader.java new file mode 100644 index 0000000..337ff39 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/SupportCursorLoader.java @@ -0,0 +1,123 @@ +package se.emilsjolander.sprinkles; + +import java.util.List; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.support.v4.content.AsyncTaskLoader; + + +class SupportCursorLoader extends AsyncTaskLoader { + + private final ForceLoadContentObserver mObserver; + + private String mSql; + private List> mDependencies; + private Cursor mCursor; + + public SupportCursorLoader(Context context, String sql, + List> dependencies) { + super(context); + mObserver = new ForceLoadContentObserver(); + mSql = sql; + mDependencies = dependencies; + } + + /* Runs on a worker thread */ + @Override + public Cursor loadInBackground() { + final SQLiteDatabase db = DbOpenHelper.getInstance(); + Cursor cursor = db.rawQuery(mSql, null); + + if (cursor != null) { + // Ensure the cursor window is filled + cursor.getCount(); + + if (mDependencies != null) { + cursor.registerContentObserver(mObserver); + for (Class dependency : mDependencies) { + getContext().getContentResolver().registerContentObserver( + Utils.getNotificationUri(dependency), false, mObserver); + } + } + } + return cursor; + } + + /* Runs on the UI thread */ + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + // An async query came in while the loader is stopped + if (cursor != null) { + cursor.close(); + } + return; + } + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (isStarted()) { + super.deliverResult(cursor); + } + + if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { + oldCursor.close(); + } + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is + * ready the callbacks will be called on the UI thread. If a previous load + * has been completed and is still valid the result may be passed to the + * callbacks immediately. + * + * Must be called from the UI thread + */ + @Override + protected void onStartLoading() { + if (mCursor != null) { + deliverResult(mCursor); + } + if (takeContentChanged() || mCursor == null) { + forceLoad(); + } + } + + /** + * Must be called from the UI thread + */ + @Override + protected void onStopLoading() { + // Attempt to cancel the current load task if possible. + cancelLoad(); + } + + @Override + public void onCanceled(Cursor cursor) { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + + @Override + protected void onAbandon() { + super.onAbandon(); + getContext().getContentResolver().unregisterContentObserver(mObserver); + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + + if (mCursor != null && !mCursor.isClosed()) { + mCursor.close(); + } + mCursor = null; + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/Transaction.java b/library/src/se/emilsjolander/sprinkles/Transaction.java new file mode 100644 index 0000000..6bdf647 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/Transaction.java @@ -0,0 +1,61 @@ +package se.emilsjolander.sprinkles; + +import java.util.ArrayList; +import java.util.List; + +import android.content.ContentValues; +import android.database.sqlite.SQLiteDatabase; + +public final class Transaction { + + interface OnTransactionCommittedListener { + void onTransactionCommitted(); + } + + private SQLiteDatabase mDb; + private boolean mSuccessful; + private List mOnTransactionCommittedListeners = new ArrayList(); + + public Transaction() { + mDb = DbOpenHelper.getInstance(); + mDb.beginTransaction(); + } + + public void setSuccessful(boolean successful) { + mSuccessful = successful; + } + + public boolean isSuccessful() { + return mSuccessful; + } + + public void finish() { + if (mSuccessful) { + mDb.setTransactionSuccessful(); + } + mDb.endTransaction(); + + if (mSuccessful) { + for (OnTransactionCommittedListener listener : mOnTransactionCommittedListeners) { + listener.onTransactionCommitted(); + } + } + } + + long insert(String table, ContentValues values) { + return mDb.insert(table, null, values); + } + + int update(String table, ContentValues values, String where) { + return mDb.update(table, values, where, null); + } + + int delete(String table, String where) { + return mDb.delete(table, where, null); + } + + void addOnTransactionCommittedListener(OnTransactionCommittedListener listener) { + mOnTransactionCommittedListeners.add(listener); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/Utils.java b/library/src/se/emilsjolander/sprinkles/Utils.java new file mode 100644 index 0000000..06dbafc --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/Utils.java @@ -0,0 +1,273 @@ +package se.emilsjolander.sprinkles; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import se.emilsjolander.sprinkles.annotations.AutoIncrementPrimaryKey; +import se.emilsjolander.sprinkles.annotations.CascadeDelete; +import se.emilsjolander.sprinkles.annotations.Column; +import se.emilsjolander.sprinkles.annotations.ForeignKey; +import se.emilsjolander.sprinkles.annotations.PrimaryKey; +import se.emilsjolander.sprinkles.annotations.Table; +import se.emilsjolander.sprinkles.exceptions.AutoIncrementMustBeIntegerException; +import se.emilsjolander.sprinkles.exceptions.CannotCascadeDeleteNonForeignKey; +import se.emilsjolander.sprinkles.exceptions.DuplicateColumnException; +import se.emilsjolander.sprinkles.exceptions.EmptyTableException; +import se.emilsjolander.sprinkles.exceptions.MultipleAutoIncrementFieldsException; +import se.emilsjolander.sprinkles.exceptions.NoPrimaryKeysException; +import se.emilsjolander.sprinkles.exceptions.NoSuchSqlTypeExistsException; +import se.emilsjolander.sprinkles.exceptions.NoTableAnnotationException; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.net.Uri; + +class Utils { + + static Uri getNotificationUri(Class resultClass) { + return Uri.parse("sprinkles://"+resultClass.getAnnotation(Table.class).value()); + } + + static T getModelFromCursor(Class resultClass, Cursor c) { + try { + T result = resultClass.newInstance(); + final List columns = Utils.getColumns(resultClass); + for (ColumnField column : columns) { + column.field.setAccessible(true); + final Object value = column.field.get(result); + if (value instanceof String) { + column.field.set(result, c.getString(c.getColumnIndexOrThrow(column.name))); + } else if (value instanceof Integer) { + column.field.set(result, c.getInt(c.getColumnIndexOrThrow(column.name))); + } else if (value instanceof Long) { + column.field.set(result, c.getLong(c.getColumnIndexOrThrow(column.name))); + } else if (value instanceof Boolean) { + column.field.set(result, c.getInt(c.getColumnIndexOrThrow(column.name)) > 0); + } else if (value instanceof Float) { + column.field.set(result, c.getFloat(c.getColumnIndexOrThrow(column.name))); + } else if (value instanceof Double) { + column.field.set(result, c.getDouble(c.getColumnIndexOrThrow(column.name))); + } + } + return result; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + static String getTableName(Class clazz) { + if (clazz.isAnnotationPresent(Table.class)) { + Table table = clazz.getAnnotation(Table.class); + return table.value(); + } + throw new NoTableAnnotationException(); + } + + static String insertSqlArgs(String sql, Object[] args) { + if (args == null) { + return sql; + } + for (Object o : args) { + if (o instanceof Number) { + sql = sql.replaceFirst("\\?", o.toString()); + } else { + String escapedString = DatabaseUtils.sqlEscapeString(o + .toString()); + sql = sql.replaceFirst("\\?", escapedString); + } + } + return sql; + } + + static String getWhereStatement(Model m) { + final List columns = Utils.getColumns(m.getClass()); + final List primaryColumn = new ArrayList(); + for (ColumnField column : columns) { + if (column.isPrimaryKey || column.isAutoIncrementPrimaryKey) { + primaryColumn.add(column); + } + } + final StringBuilder where = new StringBuilder(); + for (int i = 0; i < primaryColumn.size(); i++) { + final ColumnField column = primaryColumn.get(i); + where.append(column.name); + where.append("=?"); + + // split where statements with AND + if (i < primaryColumn.size()-1) { + where.append(" AND "); + } + } + final Object[] args = new Object[primaryColumn.size()]; + for (int i = 0; i < primaryColumn.size(); i++) { + final ColumnField column = primaryColumn.get(i); + column.field.setAccessible(true); + try { + args[i] = column.field.get(m); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return Utils.insertSqlArgs(where.toString(), args); + } + + static ContentValues getContentValues(Model model) { + final List columns = getColumns(model.getClass()); + final ContentValues values = new ContentValues(); + + for (ColumnField column : columns) { + if (column.isAutoIncrementPrimaryKey) { + continue; + } + column.field.setAccessible(true); + Object value; + try { + value = column.field.get(model); + } catch (Exception e) { + throw new RuntimeException(e); + } + if (value != null) { + if (value instanceof String) { + values.put(column.name, (String) value); + } else if (value instanceof Integer) { + values.put(column.name, (Integer) value); + } else if (value instanceof Long) { + values.put(column.name, (Long) value); + } else if (value instanceof Boolean) { + values.put(column.name, (Boolean) value); + } else if (value instanceof Float) { + values.put(column.name, (Float) value); + } else if (value instanceof Double) { + values.put(column.name, (Double) value); + } + } + } + + return values; + } + + static List getColumns(Class clazz) { + final Field[] fields = getAllDeclaredField(clazz, Model.class); + final List columns = new ArrayList(); + + for (Field field : fields) { + if (field + .isAnnotationPresent(Column.class)) { + ColumnField column = new ColumnField(); + column.name = field.getAnnotation(Column.class) + .value(); + + if (columns.contains(column)) { + throw new DuplicateColumnException(column.name); + } + + column.isAutoIncrementPrimaryKey = field + .isAnnotationPresent(AutoIncrementPrimaryKey.class); + column.isForeignKey = field + .isAnnotationPresent(ForeignKey.class); + column.isPrimaryKey = field + .isAnnotationPresent(PrimaryKey.class); + column.isCascadeDelete = field + .isAnnotationPresent(CascadeDelete.class); + + if (column.isForeignKey) { + column.foreignKey = field.getAnnotation(ForeignKey.class) + .value(); + } else if (column.isCascadeDelete) { + throw new CannotCascadeDeleteNonForeignKey(); + } + + column.type = getSqlType(field); + + if (column.isAutoIncrementPrimaryKey && !column.type.equals("INTEGER")) { + throw new AutoIncrementMustBeIntegerException(column.name); + } + + if (column.isAutoIncrementPrimaryKey && (column.isPrimaryKey || column.isForeignKey)) { + throw new IllegalStateException("A @AutoIncrementPrimaryKey field may not also be an @PrimaryKey or @ForeignKey field"); + } + + column.field = field; + columns.add(column); + } + } + + if (columns.isEmpty()) { + throw new EmptyTableException(clazz.getName()); + } + + int numberOfAutoIncrementFields = 0; + for (ColumnField column : columns) { + if (column.isAutoIncrementPrimaryKey) { + numberOfAutoIncrementFields++; + if (numberOfAutoIncrementFields > 1) { + throw new MultipleAutoIncrementFieldsException(); + } + } + } + + int numberOfPrimaryKeys = 0; + for (ColumnField column : columns) { + if (column.isPrimaryKey) { + numberOfPrimaryKeys++; + } + } + + if (numberOfAutoIncrementFields > 0 && numberOfPrimaryKeys > 0) { + throw new IllegalStateException("A model with a field marked as @AutoIncrementPrimaryKey may not mark any other field with @PrimaryKey"); + } + + if (numberOfAutoIncrementFields == 0 && numberOfPrimaryKeys == 0) { + throw new NoPrimaryKeysException(); + } + + return columns; + } + + @SuppressWarnings("unchecked") + private static Field[] getAllDeclaredField(Class clazz, + Class stopAt) { + Field[] result = new Field[] {}; + while (!clazz.equals(stopAt)) { + result = concatFieldArrays(result, clazz.getDeclaredFields()); + clazz = (Class) clazz.getSuperclass(); + } + return result; + } + + private static Field[] concatFieldArrays(Field[] one, Field[] two) { + final int length = one.length + two.length; + final Field[] result = new Field[length]; + for (int i = 0; i < length; i++) { + if (i < one.length) { + result[i] = one[i]; + } else { + result[i] = two[i - one.length]; + } + } + return result; + } + + private static String getSqlType(Field field) { + Class type = field.getType(); + if (isTypeOneOf(type, int.class, long.class, boolean.class, Integer.class, Long.class, Boolean.class)) { + return "INTEGER"; + } else if(isTypeOneOf(type, float.class, double.class, Float.class, Double.class)) { + return "REAL"; + } else if(isTypeOneOf(type, String.class)) { + return "TEXT"; + } + throw new NoSuchSqlTypeExistsException(field.getName()); + } + + private static boolean isTypeOneOf(Class type, Class... types) { + for (Class t : types) { + if (type.equals(t)) { + return true; + } + } + return false; + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/annotations/AutoIncrementPrimaryKey.java b/library/src/se/emilsjolander/sprinkles/annotations/AutoIncrementPrimaryKey.java new file mode 100644 index 0000000..7948262 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/annotations/AutoIncrementPrimaryKey.java @@ -0,0 +1,12 @@ +package se.emilsjolander.sprinkles.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface AutoIncrementPrimaryKey { + +} diff --git a/library/src/se/emilsjolander/sprinkles/annotations/CascadeDelete.java b/library/src/se/emilsjolander/sprinkles/annotations/CascadeDelete.java new file mode 100644 index 0000000..7e1bf19 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/annotations/CascadeDelete.java @@ -0,0 +1,12 @@ +package se.emilsjolander.sprinkles.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CascadeDelete { + +} diff --git a/library/src/se/emilsjolander/sprinkles/annotations/Column.java b/library/src/se/emilsjolander/sprinkles/annotations/Column.java new file mode 100644 index 0000000..bf771bc --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/annotations/Column.java @@ -0,0 +1,12 @@ +package se.emilsjolander.sprinkles.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Column { + String value(); +} diff --git a/library/src/se/emilsjolander/sprinkles/annotations/ForeignKey.java b/library/src/se/emilsjolander/sprinkles/annotations/ForeignKey.java new file mode 100644 index 0000000..72011a1 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/annotations/ForeignKey.java @@ -0,0 +1,12 @@ +package se.emilsjolander.sprinkles.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ForeignKey { + String value(); +} diff --git a/library/src/se/emilsjolander/sprinkles/annotations/PrimaryKey.java b/library/src/se/emilsjolander/sprinkles/annotations/PrimaryKey.java new file mode 100644 index 0000000..988e1aa --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/annotations/PrimaryKey.java @@ -0,0 +1,12 @@ +package se.emilsjolander.sprinkles.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface PrimaryKey { + +} diff --git a/library/src/se/emilsjolander/sprinkles/annotations/Table.java b/library/src/se/emilsjolander/sprinkles/annotations/Table.java new file mode 100644 index 0000000..9746681 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/annotations/Table.java @@ -0,0 +1,12 @@ +package se.emilsjolander.sprinkles.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Table { + String value(); +} diff --git a/library/src/se/emilsjolander/sprinkles/exceptions/AutoIncrementMustBeIntegerException.java b/library/src/se/emilsjolander/sprinkles/exceptions/AutoIncrementMustBeIntegerException.java new file mode 100644 index 0000000..a7a837b --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/exceptions/AutoIncrementMustBeIntegerException.java @@ -0,0 +1,14 @@ +package se.emilsjolander.sprinkles.exceptions; + +public class AutoIncrementMustBeIntegerException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 3214776964727149312L; + + public AutoIncrementMustBeIntegerException(String column) { + super(String.format("The column %s was marked as a @AutoIncrement field but is not a int or a long", column)); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/exceptions/CannotCascadeDeleteNonForeignKey.java b/library/src/se/emilsjolander/sprinkles/exceptions/CannotCascadeDeleteNonForeignKey.java new file mode 100644 index 0000000..7167e7c --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/exceptions/CannotCascadeDeleteNonForeignKey.java @@ -0,0 +1,14 @@ +package se.emilsjolander.sprinkles.exceptions; + +public class CannotCascadeDeleteNonForeignKey extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 4626270583942735558L; + + public CannotCascadeDeleteNonForeignKey() { + super("A @CascadeDelete annotation may only be present on a field with a @ForeignKey annotation"); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/exceptions/DuplicateColumnException.java b/library/src/se/emilsjolander/sprinkles/exceptions/DuplicateColumnException.java new file mode 100644 index 0000000..31dbec3 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/exceptions/DuplicateColumnException.java @@ -0,0 +1,14 @@ +package se.emilsjolander.sprinkles.exceptions; + +public class DuplicateColumnException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = -3093784258060286873L; + + public DuplicateColumnException(String columnName) { + super(String.format("Column %s is declared mutiple times", columnName)); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/exceptions/EmptyTableException.java b/library/src/se/emilsjolander/sprinkles/exceptions/EmptyTableException.java new file mode 100644 index 0000000..7bd4e78 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/exceptions/EmptyTableException.java @@ -0,0 +1,14 @@ +package se.emilsjolander.sprinkles.exceptions; + +public class EmptyTableException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = -8685887532184477033L; + + public EmptyTableException(String clazz) { + super(String.format("Class %s does not declare any fields", clazz)); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/exceptions/MultipleAutoIncrementFieldsException.java b/library/src/se/emilsjolander/sprinkles/exceptions/MultipleAutoIncrementFieldsException.java new file mode 100644 index 0000000..d2fb9f5 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/exceptions/MultipleAutoIncrementFieldsException.java @@ -0,0 +1,14 @@ +package se.emilsjolander.sprinkles.exceptions; + +public class MultipleAutoIncrementFieldsException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 432376145867788649L; + + public MultipleAutoIncrementFieldsException() { + super("No table is allowed to declare more than one @AutoIncrement field"); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/exceptions/NoPrimaryKeysException.java b/library/src/se/emilsjolander/sprinkles/exceptions/NoPrimaryKeysException.java new file mode 100644 index 0000000..ada1164 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/exceptions/NoPrimaryKeysException.java @@ -0,0 +1,14 @@ +package se.emilsjolander.sprinkles.exceptions; + +public class NoPrimaryKeysException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = -4928042808343728820L; + + public NoPrimaryKeysException() { + super("Every model must have atleast one primary key!"); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/exceptions/NoSuchColumnFoundException.java b/library/src/se/emilsjolander/sprinkles/exceptions/NoSuchColumnFoundException.java new file mode 100644 index 0000000..f3deb64 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/exceptions/NoSuchColumnFoundException.java @@ -0,0 +1,14 @@ +package se.emilsjolander.sprinkles.exceptions; + +public class NoSuchColumnFoundException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 2204104607828295235L; + + public NoSuchColumnFoundException(String column) { + super(String.format("Column %s does not exist", column)); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/exceptions/NoSuchSqlTypeExistsException.java b/library/src/se/emilsjolander/sprinkles/exceptions/NoSuchSqlTypeExistsException.java new file mode 100644 index 0000000..5a1d8b7 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/exceptions/NoSuchSqlTypeExistsException.java @@ -0,0 +1,14 @@ +package se.emilsjolander.sprinkles.exceptions; + +public class NoSuchSqlTypeExistsException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 732723846870837880L; + + public NoSuchSqlTypeExistsException(String fieldName) { + super(String.format("Field %s has a type that cannot be converted to an sql type", fieldName)); + } + +} diff --git a/library/src/se/emilsjolander/sprinkles/exceptions/NoTableAnnotationException.java b/library/src/se/emilsjolander/sprinkles/exceptions/NoTableAnnotationException.java new file mode 100644 index 0000000..3545ef1 --- /dev/null +++ b/library/src/se/emilsjolander/sprinkles/exceptions/NoTableAnnotationException.java @@ -0,0 +1,14 @@ +package se.emilsjolander.sprinkles.exceptions; + +public class NoTableAnnotationException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = -7192150829452806874L; + + public NoTableAnnotationException() { + super("Your model must be annotated with an @Table annotation"); + } + +} diff --git a/sample/.settings/org.eclipse.jdt.core.prefs b/sample/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..b080d2d --- /dev/null +++ b/sample/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/sample/AndroidManifest.xml b/sample/AndroidManifest.xml new file mode 100644 index 0000000..2bf112b --- /dev/null +++ b/sample/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + diff --git a/sample/libs/android-support-v4.jar b/sample/libs/android-support-v4.jar new file mode 100644 index 0000000..cf12d28 Binary files /dev/null and b/sample/libs/android-support-v4.jar differ diff --git a/sample/proguard-project.txt b/sample/proguard-project.txt new file mode 100644 index 0000000..f2fe155 --- /dev/null +++ b/sample/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/sample/project.properties b/sample/project.properties new file mode 100644 index 0000000..7143bfd --- /dev/null +++ b/sample/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-18 +android.library.reference.1=../library diff --git a/sample/res/drawable-hdpi/ic_launcher.png b/sample/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..96a442e Binary files /dev/null and b/sample/res/drawable-hdpi/ic_launcher.png differ diff --git a/sample/res/drawable-xhdpi/ic_launcher.png b/sample/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..71c6d76 Binary files /dev/null and b/sample/res/drawable-xhdpi/ic_launcher.png differ diff --git a/sample/res/layout/activity_main.xml b/sample/res/layout/activity_main.xml new file mode 100644 index 0000000..168c9b8 --- /dev/null +++ b/sample/res/layout/activity_main.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/sample/res/values/dimens.xml b/sample/res/values/dimens.xml new file mode 100644 index 0000000..55c1e59 --- /dev/null +++ b/sample/res/values/dimens.xml @@ -0,0 +1,7 @@ + + + + 16dp + 16dp + + diff --git a/sample/res/values/strings.xml b/sample/res/values/strings.xml new file mode 100644 index 0000000..a955e0f --- /dev/null +++ b/sample/res/values/strings.xml @@ -0,0 +1,8 @@ + + + + SprinklesSample + Settings + Hello world! + + diff --git a/sample/res/values/styles.xml b/sample/res/values/styles.xml new file mode 100644 index 0000000..18ae4a1 --- /dev/null +++ b/sample/res/values/styles.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/sample/src/se/emilsjolander/sprinkles/MainActivity.java b/sample/src/se/emilsjolander/sprinkles/MainActivity.java new file mode 100644 index 0000000..7e210af --- /dev/null +++ b/sample/src/se/emilsjolander/sprinkles/MainActivity.java @@ -0,0 +1,50 @@ +package se.emilsjolander.sprinkles; + +import java.util.List; + +import se.emilsjolander.sprinkles.Query.OnQueryResultHandler; +import se.emilsjolander.sprinkles.models.Note; +import se.emilsjolander.sprinkles.models.NoteTagLink; +import se.emilsjolander.sprinkles.models.Tag; +import android.app.Activity; +import android.os.Bundle; +import android.widget.Toast; + +public class MainActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Note myNote = new Note(); + myNote.setContent(""+System.currentTimeMillis()); + myNote.save(); + + Tag myTag = Query.one(Tag.class, "select * from Tags").get(); + if (myTag == null) { + myTag = new Tag(); + myTag.setName("MyTag"); + myTag.setColor(0xffff3333); + myTag.save(); + } + + NoteTagLink link = new NoteTagLink(); + link.setNoteId(myNote.getId()); + link.setTagId(myTag.getId()); + link.save(); + + Query.many(Note.class, "select Notes.* from Notes " + + "inner join NoteTagLinks on Notes.id=NoteTagLinks.note_id " + + "where NoteTagLinks.tag_id=?", myTag.getId()) + .getAsync(getLoaderManager(), new OnQueryResultHandler>() { + + @Override + public void onResult(List result) { + Toast.makeText(MainActivity.this, ""+result.size(), Toast.LENGTH_SHORT).show(); + } + + }); + } + +} diff --git a/sample/src/se/emilsjolander/sprinkles/MyApplication.java b/sample/src/se/emilsjolander/sprinkles/MyApplication.java new file mode 100644 index 0000000..b49499c --- /dev/null +++ b/sample/src/se/emilsjolander/sprinkles/MyApplication.java @@ -0,0 +1,23 @@ +package se.emilsjolander.sprinkles; + +import se.emilsjolander.sprinkles.models.Note; +import se.emilsjolander.sprinkles.models.NoteTagLink; +import se.emilsjolander.sprinkles.models.Tag; +import android.app.Application; + +public class MyApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + + Sprinkles sprinkles = Sprinkles.getInstance(getApplicationContext()); + + Migration initialMigration = new Migration(); + initialMigration.createTable(Note.class); + initialMigration.createTable(Tag.class); + initialMigration.createTable(NoteTagLink.class); + sprinkles.addMigration(initialMigration); + } + +} diff --git a/sample/src/se/emilsjolander/sprinkles/models/Note.java b/sample/src/se/emilsjolander/sprinkles/models/Note.java new file mode 100644 index 0000000..1b59495 --- /dev/null +++ b/sample/src/se/emilsjolander/sprinkles/models/Note.java @@ -0,0 +1,28 @@ +package se.emilsjolander.sprinkles.models; + +import se.emilsjolander.sprinkles.Model; +import se.emilsjolander.sprinkles.annotations.AutoIncrementPrimaryKey; +import se.emilsjolander.sprinkles.annotations.Column; +import se.emilsjolander.sprinkles.annotations.Table; + +@Table("Notes") +public class Note extends Model { + + @AutoIncrementPrimaryKey + @Column("id") private long id; + + @Column("content") private String content; + + public long getId() { + return id; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + +} diff --git a/sample/src/se/emilsjolander/sprinkles/models/NoteTagLink.java b/sample/src/se/emilsjolander/sprinkles/models/NoteTagLink.java new file mode 100644 index 0000000..9d2f3a9 --- /dev/null +++ b/sample/src/se/emilsjolander/sprinkles/models/NoteTagLink.java @@ -0,0 +1,31 @@ +package se.emilsjolander.sprinkles.models; + +import se.emilsjolander.sprinkles.Model; +import se.emilsjolander.sprinkles.annotations.CascadeDelete; +import se.emilsjolander.sprinkles.annotations.Column; +import se.emilsjolander.sprinkles.annotations.ForeignKey; +import se.emilsjolander.sprinkles.annotations.PrimaryKey; +import se.emilsjolander.sprinkles.annotations.Table; + +@Table("NoteTagLinks") +public class NoteTagLink extends Model { + + @PrimaryKey + @CascadeDelete + @ForeignKey("Notes(id)") + @Column("note_id") private long noteId; + + @PrimaryKey + @CascadeDelete + @ForeignKey("Tags(id)") + @Column("tag_id") private long tagId; + + public void setNoteId(long noteId) { + this.noteId = noteId; + } + + public void setTagId(long tagId) { + this.tagId = tagId; + } + +} diff --git a/sample/src/se/emilsjolander/sprinkles/models/Tag.java b/sample/src/se/emilsjolander/sprinkles/models/Tag.java new file mode 100644 index 0000000..0253f5e --- /dev/null +++ b/sample/src/se/emilsjolander/sprinkles/models/Tag.java @@ -0,0 +1,37 @@ +package se.emilsjolander.sprinkles.models; + +import se.emilsjolander.sprinkles.Model; +import se.emilsjolander.sprinkles.annotations.AutoIncrementPrimaryKey; +import se.emilsjolander.sprinkles.annotations.Column; +import se.emilsjolander.sprinkles.annotations.Table; + +@Table("Tags") +public class Tag extends Model { + + @AutoIncrementPrimaryKey + @Column("id") private long id; + + @Column("name") private String name; + @Column("color") private int color; + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + +} diff --git a/sprinkles.png b/sprinkles.png new file mode 100644 index 0000000..11f4ad1 Binary files /dev/null and b/sprinkles.png differ diff --git a/sprinkles.sketch/Data b/sprinkles.sketch/Data new file mode 100644 index 0000000..dac6e38 Binary files /dev/null and b/sprinkles.sketch/Data differ diff --git a/sprinkles.sketch/QuickLook/Preview.png b/sprinkles.sketch/QuickLook/Preview.png new file mode 100644 index 0000000..4a5b383 Binary files /dev/null and b/sprinkles.sketch/QuickLook/Preview.png differ diff --git a/sprinkles.sketch/QuickLook/Thumbnail.png b/sprinkles.sketch/QuickLook/Thumbnail.png new file mode 100644 index 0000000..db198dc Binary files /dev/null and b/sprinkles.sketch/QuickLook/Thumbnail.png differ diff --git a/sprinkles.sketch/fonts b/sprinkles.sketch/fonts new file mode 100644 index 0000000..e69de29 diff --git a/sprinkles.sketch/version b/sprinkles.sketch/version new file mode 100644 index 0000000..da2d398 --- /dev/null +++ b/sprinkles.sketch/version @@ -0,0 +1 @@ +14 \ No newline at end of file