14 | * This represents a message. This class is never instantiated, it just contains static definitions 15 | * that can be used to access messages using the {@link ContentProvider} framework. 16 | *
17 | * 18 | *19 | * The database table name is a sanitized, lower-cased version of this classname. In this case it is 20 | * "message". You can override this by using the {@link DBTable} annotation on this class. 21 | *
22 | * 23 | */ 24 | public class Message implements ContentItem { 25 | 26 | // Column definitions /////////////////////////////////// 27 | 28 | // ContentItem contains one column definition for the BaseColumns._ID which 29 | // defines the primary key. 30 | 31 | // An example column that is automatically set to the current date/time. 32 | // The value of the string is the column name. 33 | @DBColumn(type = DatetimeColumn.class, defaultValue = DatetimeColumn.NOW_IN_MILLISECONDS) 34 | public static final String CREATED_DATE = "created"; 35 | 36 | // An example text column, representing 37 | @DBColumn(type = TextColumn.class) 38 | public static final String TITLE = "title"; 39 | 40 | @DBColumn(type = TextColumn.class) 41 | public static final String BODY = "body"; 42 | 43 | // ////////////////////////////////////////////////////// 44 | 45 | // This defines the path component of the content URI. 46 | // For most instances, it's best to just use the classname here: 47 | public static final String PATH = "message"; 48 | 49 | // The SimpleContentProvider constructs content URIs based on your provided 50 | // path and authority. 51 | // This constant is not necessary, but is very handy for doing queries. 52 | public static final Uri CONTENT_URI = ProviderUtils.toContentUri( 53 | SampleProvider.AUTHORITY, PATH); 54 | 55 | /** 56 | *57 | * The content type representing a message item. 58 | *
59 | * 60 | *61 | * This content type is automatically generated based on the provider name and the table name. 62 | * See {@link ProviderUtils#toItemType(String, String)} for details. 63 | *
64 | */ 65 | public static final String CONTENT_TYPE_ITEM = "vnd.android.cursor.item/vnd.edu.mit.mobile.android.content.example.sampleprovider.message"; 66 | 67 | /** 68 | * The content type representing a list of messages. 69 | */ 70 | public static final String CONTENT_TYPE_DIR = "vnd.android.cursor.dir/vnd.edu.mit.mobile.android.content.example.sampleprovider.message"; 71 | 72 | } 73 | -------------------------------------------------------------------------------- /example/src/edu/mit/mobile/android/content/example/MessageDetail.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.content.example; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.app.Dialog; 6 | import android.content.DialogInterface; 7 | import android.content.DialogInterface.OnClickListener; 8 | import android.content.Intent; 9 | import android.database.Cursor; 10 | import android.net.Uri; 11 | import android.os.AsyncTask; 12 | import android.os.Bundle; 13 | import android.text.format.DateUtils; 14 | import android.view.Menu; 15 | import android.view.MenuItem; 16 | import android.view.Window; 17 | import android.widget.TextView; 18 | import android.widget.Toast; 19 | 20 | /** 21 | * A detail view of a single message. Responds to the VIEW intent for a single message content item. 22 | * 23 | * @author Steve Pomeroy 24 | * 25 | */ 26 | public class MessageDetail extends Activity { 27 | private TextView mTitle, mBody, mDate; 28 | 29 | private static final String[] PROJECTION = { Message._ID, Message.TITLE, Message.BODY, 30 | Message.CREATED_DATE }; 31 | 32 | private static final int DIALOG_CONFIRM_DELETE = 100; 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 37 | super.onCreate(savedInstanceState); 38 | setContentView(R.layout.detail); 39 | 40 | mTitle = (TextView) findViewById(R.id.title); 41 | mBody = (TextView) findViewById(R.id.body); 42 | mDate = (TextView) findViewById(R.id.date); 43 | } 44 | 45 | @Override 46 | protected void onResume() { 47 | super.onResume(); 48 | 49 | final Intent intent = getIntent(); 50 | 51 | final Uri message = intent.getData(); 52 | final String action = intent.getAction(); 53 | 54 | // While we declared an intent filter in the manifest specifying the 55 | // VIEW action and the message content type, it's still possible that 56 | // the activity might be started explicitly using the classname. This is 57 | // just a sanity check to prevent any null pointer exceptions or 58 | // to alert of any programmer errors. 59 | if (Intent.ACTION_VIEW.equals(action) && message != null) { 60 | 61 | // load the content on a background thread 62 | new ContentLoadTask().execute(message); 63 | } else { 64 | Toast.makeText( 65 | this, 66 | MessageDetail.class.getSimpleName() 67 | + " doesn't know how to handle the intent: " + intent, 68 | Toast.LENGTH_LONG).show(); 69 | finish(); 70 | } 71 | } 72 | 73 | @Override 74 | public boolean onCreateOptionsMenu(Menu menu) { 75 | // the item context menu can be easily reused for a detail action bar 76 | getMenuInflater().inflate(R.menu.item_context, menu); 77 | 78 | // this is the view screen, so we hide this. 79 | menu.findItem(R.id.view).setVisible(false); 80 | return super.onCreateOptionsMenu(menu); 81 | } 82 | 83 | @SuppressWarnings("deprecation") 84 | @Override 85 | public boolean onOptionsItemSelected(MenuItem item) { 86 | switch (item.getItemId()) { 87 | case R.id.edit: 88 | startActivity(new Intent(Intent.ACTION_EDIT, getIntent().getData())); 89 | break; 90 | 91 | case R.id.delete: 92 | showDialog(DIALOG_CONFIRM_DELETE); 93 | break; 94 | 95 | } 96 | return super.onOptionsItemSelected(item); 97 | } 98 | 99 | @SuppressWarnings("deprecation") 100 | @Override 101 | protected Dialog onCreateDialog(int id) { 102 | switch (id) { 103 | case DIALOG_CONFIRM_DELETE: 104 | return new AlertDialog.Builder(this).setCancelable(true) 105 | .setTitle(R.string.delete_confirm_title) 106 | .setMessage(R.string.delete_confirm_prompt) 107 | .setPositiveButton(R.string.delete, mDeleteDialogListener) 108 | .setNegativeButton(android.R.string.cancel, mDeleteDialogListener).create(); 109 | default: 110 | return super.onCreateDialog(id); 111 | } 112 | } 113 | 114 | private Cursor queryDatabase(Uri message) { 115 | @SuppressWarnings("deprecation") 116 | final Cursor c = managedQuery(message, PROJECTION, null, null, null); 117 | return c; 118 | } 119 | 120 | /** 121 | * Loads the content into the Activity. 122 | * 123 | * @param message 124 | * a cursor which contains the content of the message to load. 125 | */ 126 | private void loadContent(Cursor c) { 127 | 128 | // One should always check that the returned cursor has content. The 129 | // cursor will automatically seek to the first position, so 130 | // this does both. 131 | if (c.moveToFirst()) { 132 | // If one is loading many items that all use the same column index, 133 | // it's most efficient to cache the column indexes to local 134 | // variables. Otherwise, one can just use the shortcut shown below: 135 | final String title = c.getString(c.getColumnIndex(Message.TITLE)); 136 | 137 | mTitle.setText(title); 138 | setTitle(title); 139 | 140 | mBody.setText(c.getString(c.getColumnIndex(Message.BODY))); 141 | 142 | // This shows an example of using dates. 143 | mDate.setText("Created: " 144 | 145 | // There are many useful date formatting functions in 146 | // DateUtils. 147 | + DateUtils.formatDateTime(this, 148 | c.getLong(c.getColumnIndex(Message.CREATED_DATE)), 149 | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME 150 | | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_YEAR)); 151 | } else { 152 | // If moveToFirst didn't return true, that means our URI no longer 153 | // resolves. The most common reason for this is that the content was 154 | // deleted. 155 | finish(); 156 | } 157 | } 158 | 159 | private void deleteItem() { 160 | getContentResolver().delete(getIntent().getData(), null, null); 161 | finish(); 162 | } 163 | 164 | /** 165 | * A background thread that shows an indeterminate progress bar in the window title while the 166 | * content is loading. 167 | * 168 | */ 169 | private class ContentLoadTask extends AsyncTask14 | * This creates a ContentProvider (which you must define in your AndroidManifest.xml) that you can 15 | * use to store all your data. You should only need one ContentProvider per Application, as each 16 | * ContentProvider can contain multiple types of data. 17 | *
18 | * 19 | *20 | * ContentProviders are accessed by querying content:// URIs. Content URIs have two types: "dir" and 21 | * "item". "dir" indicates that the URI represents a collection of logical items and "item" 22 | * indicates that it refers to a unique item. In {@link SimpleContentProvider}-generated providers, 23 | * this usually means that "dir" is a list of all the content of a given type, while "item" is an 24 | * individual content item, identified uniquely by its serial database row ID 25 | * {@link ContentItem#_ID}. 26 | *
27 | * 28 | *29 | * A "dir" URI ends in a non-numerical path: 30 | *
31 | * 32 | *33 | * content://AUTHORITY/PATH 34 | *35 | *
36 | * For this example, that would be: 37 | *
38 | * 39 | *40 | * content://edu.mit.mobile.android.content.example.sampleprovider/message 41 | *42 | * 43 | *
44 | * To make it easier to query in the future, that URI is stored in {@link Message#CONTENT_URI}. 45 | *
46 | * 47 | *48 | * An "item" URI builds off the "dir" URI and ends in a number. 49 | *
50 | * 51 | *52 | * content://AUTHORITY/PATH/# 53 | *54 | *
55 | * For this example, that would be: 56 | *
57 | * 58 | *59 | * content://edu.mit.mobile.android.content.example.sampleprovider/message/3 60 | *61 | * 62 | *
63 | * URIs of this type can be constructed using 64 | * {@link ContentUris#withAppendedId(android.net.Uri, long)}. 65 | *
66 | */ 67 | public class SampleProvider extends SimpleContentProvider { 68 | 69 | // Each ContentProvider must have a globally-unique authority. You can choose an arbitrary 70 | // string here, however to ensure that they will be globally-unique, best-practice is to build 71 | // one off your Application's package string. 72 | public static final String AUTHORITY = "edu.mit.mobile.android.content.example.sampleprovider"; 73 | 74 | public static final String SEARCH_PATH = null; 75 | 76 | public static final Uri SEARCH = ProviderUtils.toContentUri(AUTHORITY, 77 | getSearchPath(SEARCH_PATH)); 78 | 79 | // Every time you update your database schema, you must increment the 80 | // database version. 81 | private static final int DB_VERSION = 1; 82 | 83 | public SampleProvider() { 84 | super(AUTHORITY, DB_VERSION); 85 | 86 | // This helper is responsible for creating the tables and performing the actual database 87 | // queries. See Message for more info. 88 | final GenericDBHelper messageHelper = new GenericDBHelper(Message.class); 89 | 90 | // By wrapping the main helper like so, this will translate the query portion of the URI 91 | // (that is, the part after the "?") into a select statement to limit the results. 92 | final QuerystringWrapper queryWrapper = new QuerystringWrapper(messageHelper); 93 | 94 | // This adds a mapping between the given content:// URI path and the 95 | // helper. 96 | addDirAndItemUri(queryWrapper, Message.PATH); 97 | 98 | // the above statements can be repeated to create multiple data 99 | // stores. Each will have separate tables and URIs. 100 | 101 | // this hooks in search 102 | final SearchDBHelper searchHelper = new SearchDBHelper(); 103 | 104 | searchHelper.registerDBHelper(messageHelper, Message.CONTENT_URI, Message.TITLE, 105 | Message.BODY, Message.TITLE, Message.BODY); 106 | 107 | addSearchUri(searchHelper, SEARCH_PATH); 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 |VERB_INSERT | VERB_QUERY
54 | * @param type
55 | * the MIME type of the item to add.
56 | */
57 | public void addDirMapping(int code, DBHelper helper, int verb, String type) {
58 | if (BuildConfig.DEBUG) {
59 | Log.d(TAG, "registered dir mapping of type " + type + " to " + helper);
60 | }
61 | mDbhMap.put(code, new DBHelperMapItem(verb, false, type, helper));
62 | }
63 |
64 | /**
65 | * Makes a mapping from the code to the given DBHelper. This helper will be used to handle any
66 | * queries for items that match the given code. All other items will throw an error. Check
67 | * {@link #canHandle(int)} and {@link #canQuery(int)}, etc. first to ensure that a query will
68 | * complete.
69 | *
70 | * @param code
71 | * A unique ID representing the given URI; usually a {@link UriMatcher} code
72 | * @param helper
73 | * The helper that should be used for this code.
74 | * @param verb
75 | * The SQL verbs that should be handled by the helper. Any other requests will throw
76 | * an error. Verbs can be joined together, eg. VERB_INSERT | VERB_QUERY
77 | * @param type
78 | * the MIME type of the item to add.
79 | */
80 | public void addItemMapping(int code, DBHelper helper, int verb, String type) {
81 | if (BuildConfig.DEBUG) {
82 | Log.d(TAG, "registered item mapping of type " + type + " to " + helper);
83 | }
84 | mDbhMap.put(code, new DBHelperMapItem(verb, true, type, helper));
85 | }
86 |
87 | /**
88 | * @param code
89 | * @return true if this helper has a mapping for the given code
90 | */
91 | public boolean canHandle(int code) {
92 | return mDbhMap.get(code) != null;
93 | }
94 |
95 | /**
96 | * @param code
97 | * @return true if this helper is allowed to insert for the given code
98 | */
99 | public boolean canInsert(int code) {
100 | final DBHelperMapItem item = mDbhMap.get(code);
101 | return item != null && item.allowVerb(VERB_INSERT);
102 | }
103 |
104 | /**
105 | * @param code
106 | * @return true if this helper is allowed to query for the given code
107 | */
108 | public boolean canQuery(int code) {
109 | final DBHelperMapItem item = mDbhMap.get(code);
110 | return item != null && item.allowVerb(VERB_QUERY);
111 | }
112 |
113 | /**
114 | * @param code
115 | * @return true if this helper is allowed to update for the given code
116 | */
117 | public boolean canUpdate(int code) {
118 | final DBHelperMapItem item = mDbhMap.get(code);
119 | return item != null && item.allowVerb(VERB_UPDATE);
120 | }
121 |
122 | /**
123 | * @param code
124 | * @return true if this helper is allowed to delete for the given code
125 | */
126 | public boolean canDelete(int code) {
127 | final DBHelperMapItem item = mDbhMap.get(code);
128 | return item != null && item.allowVerb(VERB_DELETE);
129 | }
130 |
131 | /**
132 | * @param code
133 | * @return the MIME type for the given item.
134 | */
135 | public String getType(int code) {
136 | final DBHelperMapItem dbhmi = mDbhMap.get(code);
137 | if (dbhmi == null) {
138 | throw new IllegalArgumentException("no mapping for code " + code);
139 | }
140 | return dbhmi.type;
141 | }
142 |
143 | private String getVerbDescription(int verb) {
144 | String verbString = null;
145 | if ((verb & VERB_INSERT) != 0) {
146 | verbString = "insert";
147 |
148 | } else if ((verb & VERB_QUERY) != 0) {
149 | verbString = "query";
150 |
151 | } else if ((verb & VERB_UPDATE) != 0) {
152 | verbString = "update";
153 |
154 | } else if ((verb & VERB_DELETE) != 0) {
155 | verbString = "delete";
156 | }
157 | return verbString;
158 | }
159 |
160 | private DBHelperMapItem getMap(int verb, int code) {
161 | final DBHelperMapItem dbhmi = mDbhMap.get(code);
162 |
163 | if (dbhmi == null) {
164 | throw new IllegalArgumentException("No mapping for code " + code);
165 | }
166 | if ((dbhmi.verb & verb) == 0) {
167 | throw new IllegalArgumentException("Cannot " + getVerbDescription(verb) + " for code "
168 | + code);
169 | }
170 | return dbhmi;
171 | }
172 |
173 | public Uri insert(int code, ContentProvider provider, SQLiteDatabase db, Uri uri,
174 | ContentValues values) throws SQLException {
175 | final DBHelperMapItem dbhmi = getMap(VERB_INSERT, code);
176 |
177 | return dbhmi.dbHelper.insertDir(db, provider, uri, values);
178 | }
179 |
180 | public Cursor query(int code, ContentProvider provider, SQLiteDatabase db, Uri uri,
181 | String[] projection, String selection, String[] selectionArgs, String sortOrder) {
182 | final DBHelperMapItem dbhmi = getMap(VERB_QUERY, code);
183 |
184 | if (dbhmi.isItem) {
185 | return dbhmi.dbHelper.queryItem(db, uri, projection, selection, selectionArgs,
186 | sortOrder);
187 | } else {
188 | return dbhmi.dbHelper
189 | .queryDir(db, uri, projection, selection, selectionArgs, sortOrder);
190 | }
191 | }
192 |
193 | public int update(int code, ContentProvider provider, SQLiteDatabase db, Uri uri,
194 | ContentValues cv, String selection, String[] selectionArgs) {
195 | final DBHelperMapItem dbhmi = getMap(VERB_QUERY, code);
196 |
197 | if (dbhmi.isItem) {
198 | return dbhmi.dbHelper.updateItem(db, provider, uri, cv, selection, selectionArgs);
199 | } else {
200 | return dbhmi.dbHelper.updateDir(db, provider, uri, cv, selection, selectionArgs);
201 | }
202 | }
203 |
204 | public int delete(int code, ContentProvider provider, SQLiteDatabase db, Uri uri,
205 | String selection, String[] selectionArgs) {
206 | final DBHelperMapItem dbhmi = getMap(VERB_QUERY, code);
207 |
208 | if (dbhmi.isItem) {
209 | return dbhmi.dbHelper.deleteItem(db, provider, uri, selection, selectionArgs);
210 | } else {
211 | return dbhmi.dbHelper.deleteDir(db, provider, uri, selection, selectionArgs);
212 | }
213 | }
214 |
215 | private class DBHelperMapItem {
216 | public DBHelperMapItem(int verb, boolean isItem, String type, DBHelper dbHelper) {
217 | this.verb = verb;
218 | this.dbHelper = dbHelper;
219 | this.isItem = isItem;
220 | this.type = type;
221 | }
222 |
223 | public boolean allowVerb(int verb) {
224 | return (this.verb & verb) != 0;
225 | }
226 |
227 | final DBHelper dbHelper;
228 | final int verb;
229 | final boolean isItem;
230 | final String type;
231 |
232 | }
233 |
234 | public static final int VERB_INSERT = 1, VERB_QUERY = 2, VERB_UPDATE = 4, VERB_DELETE = 8,
235 | VERB_ALL = VERB_INSERT | VERB_QUERY | VERB_UPDATE | VERB_DELETE;
236 | }
237 |
--------------------------------------------------------------------------------
/src/edu/mit/mobile/android/content/DBSortOrder.java:
--------------------------------------------------------------------------------
1 | package edu.mit.mobile.android.content;
2 |
3 | /*
4 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab
5 | *
6 | * This library is free software; you can redistribute it and/or
7 | * modify it under the terms of the GNU Lesser General Public
8 | * License version 2.1 as published by the Free Software Foundation.
9 | *
10 | * This library is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | * Lesser General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Lesser General Public
16 | * License along with this library; if not, visit
17 | * http://www.gnu.org/licenses/lgpl.html
18 | */
19 | import java.lang.annotation.Documented;
20 | import java.lang.annotation.ElementType;
21 | import java.lang.annotation.Retention;
22 | import java.lang.annotation.RetentionPolicy;
23 | import java.lang.annotation.Target;
24 |
25 | /**
26 | * Annotate your {@link ContentItem} class with this in order to specify a default sort order when
27 | * queried using {@link GenericDBHelper}. It's often easiest to define the sort order as a
28 | * {@code static final String} in the class and then refer to it in this annotation value.
29 | *
30 | * @author Steve Pomeroy
31 | *
32 | */
33 | @Retention(RetentionPolicy.RUNTIME)
34 | @Target(ElementType.TYPE)
35 | @Documented
36 | public @interface DBSortOrder {
37 |
38 | /**
39 | * @return the default sort order for this type
40 | */
41 | public String value();
42 | }
43 |
--------------------------------------------------------------------------------
/src/edu/mit/mobile/android/content/DBTable.java:
--------------------------------------------------------------------------------
1 | package edu.mit.mobile.android.content;
2 | /*
3 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab
4 | *
5 | * This library is free software; you can redistribute it and/or
6 | * modify it under the terms of the GNU Lesser General Public
7 | * License version 2.1 as published by the Free Software Foundation.
8 | *
9 | * This library is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 | * Lesser General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Lesser General Public
15 | * License along with this library; if not, visit
16 | * http://www.gnu.org/licenses/lgpl.html
17 | */
18 |
19 | import java.lang.annotation.Documented;
20 | import java.lang.annotation.ElementType;
21 | import java.lang.annotation.Retention;
22 | import java.lang.annotation.RetentionPolicy;
23 | import java.lang.annotation.Target;
24 |
25 | /**
26 | * Specifies the table name for the given {@link ContentItem}. Pass a string, which will be used as
27 | * the table name.
28 | *
29 | * @author Steve Pomeroy
30 | *
31 | */
32 | @Retention(RetentionPolicy.RUNTIME)
33 | @Target(ElementType.TYPE)
34 | @Documented
35 | public @interface DBTable {
36 |
37 | /**
38 | * @return the name of the table
39 | */
40 | String value();
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/edu/mit/mobile/android/content/ForeignKeyDBHelper.java:
--------------------------------------------------------------------------------
1 | package edu.mit.mobile.android.content;
2 | /*
3 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab
4 | *
5 | * This library is free software; you can redistribute it and/or
6 | * modify it under the terms of the GNU Lesser General Public
7 | * License version 2.1 as published by the Free Software Foundation.
8 | *
9 | * This library is distributed in the hope that it will be useful,
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 | * Lesser General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU Lesser General Public
15 | * License along with this library; if not, visit
16 | * http://www.gnu.org/licenses/lgpl.html
17 | */
18 |
19 | import android.content.ContentProvider;
20 | import android.content.ContentValues;
21 | import android.database.Cursor;
22 | import android.database.SQLException;
23 | import android.database.sqlite.SQLiteDatabase;
24 | import android.net.Uri;
25 |
26 | /**
27 | * Database helper to make it easier to access foreign key relationships between a parent and a
28 | * child with a foreign key pointing to that parent.
29 | *
30 | * 31 | * relation 32 | * ↓ 33 | * [parent] → [child] 34 | * → [child 2] 35 | *36 | * 37 | * For example, you could have an BlogPost that has a relation to multiple Comments. 38 | * 39 | * Unfortunately, if your version of SQLite doesn't support foreign keys (see 40 | * {@link AndroidVersions}), this will not automatically cascade deletes for you or verify any 41 | * relationships. It will otherwise function, though; you will just need to cascade deletes by hand. 42 | * 43 | * The query function supports wildcard parent IDs. So to select all the children with any parent, 44 | * you can use {@link #WILDCARD_PATH_SEGMENT} instead of the parent's ID number. Eg.a path of 45 | * 46 | *
47 | * /parent/_all/child/ 48 | *49 | * 50 | * to get a list of all the children in any parent. 51 | * 52 | * @author steve 53 | * 54 | */ 55 | public class ForeignKeyDBHelper extends GenericDBHelper { 56 | public static final String WILDCARD_PATH_SEGMENT = "_all"; 57 | private final String mColumn; 58 | private final Class extends ContentItem> mChild; 59 | private final Class extends ContentItem> mParent; 60 | private final String mColumnQuoted; 61 | 62 | public ForeignKeyDBHelper(Class extends ContentItem> parent, 63 | Class extends ContentItem> child, String column) { 64 | super(child); 65 | mChild = child; 66 | mParent = parent; 67 | mColumn = column; 68 | mColumnQuoted = '"' + mColumn + '"'; 69 | } 70 | 71 | @Override 72 | public Uri insertDir(SQLiteDatabase db, ContentProvider provider, Uri uri, ContentValues values) 73 | throws SQLException { 74 | final long parentId = Long.valueOf(ProviderUtils.getNthPathFromEnd(uri, 1)); 75 | values.put(mColumn, parentId); 76 | return super.insertDir(db, provider, uri, values); 77 | } 78 | 79 | @Override 80 | public int updateItem(SQLiteDatabase db, ContentProvider provider, Uri uri, 81 | ContentValues values, String where, String[] whereArgs) { 82 | final String parentId = ProviderUtils.getNthPathFromEnd(uri, 2); 83 | 84 | return super.updateItem(db, provider, uri, values, 85 | ProviderUtils.addExtraWhere(where, mColumnQuoted + "=?"), 86 | ProviderUtils.addExtraWhereArgs(whereArgs, parentId)); 87 | } 88 | 89 | @Override 90 | public int updateDir(SQLiteDatabase db, ContentProvider provider, Uri uri, 91 | ContentValues values, String where, String[] whereArgs) { 92 | final String parentId = ProviderUtils.getNthPathFromEnd(uri, 1); 93 | 94 | return super.updateDir(db, provider, uri, values, 95 | ProviderUtils.addExtraWhere(where, mColumnQuoted + "=?"), 96 | ProviderUtils.addExtraWhereArgs(whereArgs, parentId)); 97 | } 98 | 99 | @Override 100 | public int deleteItem(SQLiteDatabase db, ContentProvider provider, Uri uri, String where, 101 | String[] whereArgs) { 102 | final String parentId = ProviderUtils.getNthPathFromEnd(uri, 2); 103 | 104 | return super.deleteItem(db, provider, uri, 105 | ProviderUtils.addExtraWhere(where, mColumnQuoted + "=?"), 106 | ProviderUtils.addExtraWhereArgs(whereArgs, parentId)); 107 | } 108 | 109 | @Override 110 | public int deleteDir(SQLiteDatabase db, ContentProvider provider, Uri uri, String where, 111 | String[] whereArgs) { 112 | final String parentId = ProviderUtils.getNthPathFromEnd(uri, 1); 113 | 114 | return super.deleteDir(db, provider, uri, 115 | ProviderUtils.addExtraWhere(where, mColumnQuoted + "=?"), 116 | ProviderUtils.addExtraWhereArgs(whereArgs, parentId)); 117 | } 118 | 119 | @Override 120 | public Cursor queryDir(SQLiteDatabase db, Uri uri, String[] projection, String selection, 121 | String[] selectionArgs, String sortOrder) { 122 | final String parentId = ProviderUtils.getNthPathFromEnd(uri, 1); 123 | 124 | if (WILDCARD_PATH_SEGMENT.equals(parentId)) { 125 | return super.queryDir(db, uri, projection, selection, selectionArgs, 126 | sortOrder != null ? sortOrder : getDefaultSortOrder()); 127 | 128 | } else { 129 | return super.queryDir(db, uri, projection, 130 | ProviderUtils.addExtraWhere(selection, mColumnQuoted + "=?"), 131 | ProviderUtils.addExtraWhereArgs(selectionArgs, parentId), 132 | sortOrder != null ? sortOrder : getDefaultSortOrder()); 133 | } 134 | } 135 | 136 | @Override 137 | public Cursor queryItem(SQLiteDatabase db, Uri uri, String[] projection, String selection, 138 | String[] selectionArgs, String sortOrder) { 139 | final String parentId = ProviderUtils.getNthPathFromEnd(uri, 2); 140 | 141 | if (WILDCARD_PATH_SEGMENT.equals(parentId)) { 142 | return super.queryItem(db, uri, projection, selection, selectionArgs, 143 | sortOrder != null ? sortOrder : getDefaultSortOrder()); 144 | 145 | } else { 146 | return super.queryItem(db, uri, projection, 147 | ProviderUtils.addExtraWhere(selection, mColumnQuoted + "=?"), 148 | ProviderUtils.addExtraWhereArgs(selectionArgs, parentId), 149 | sortOrder != null ? sortOrder : getDefaultSortOrder()); 150 | } 151 | } 152 | 153 | @Override 154 | public void createTables(SQLiteDatabase db) throws SQLGenerationException { 155 | if (!mChild.equals(mParent)) { 156 | super.createTables(db); 157 | } 158 | } 159 | 160 | @Override 161 | public void upgradeTables(SQLiteDatabase db, int oldVersion, int newVersion) { 162 | if (!mChild.equals(mParent)) { 163 | super.upgradeTables(db, oldVersion, newVersion); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/content/ForeignKeyManager.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.content; 2 | /* 3 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 4 | * 5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License version 2.1 as published by the Free Software Foundation. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, visit 16 | * http://www.gnu.org/licenses/lgpl.html 17 | */ 18 | 19 | import android.content.ContentResolver; 20 | import android.content.ContentUris; 21 | import android.content.ContentValues; 22 | import android.database.Cursor; 23 | import android.net.Uri; 24 | import android.provider.BaseColumns; 25 | 26 | /** 27 | * A helpful class that can make using tables with foreign key relations easier. 28 | * 29 | * To use, simply add a static instance of this in your {@link ContentItem} class. 30 | * 31 | * @author steve 32 | * 33 | */ 34 | public class ForeignKeyManager implements Manager { 35 | private final Class extends ContentItem> mChild; 36 | private final String mPath; 37 | private final String mSortOrder; 38 | 39 | /** 40 | * 41 | * @param child 42 | * the child class that has this class as a foreign key 43 | * @param childRelationshipPath 44 | * the path that is used in the URI to refer to this relationship 45 | */ 46 | public ForeignKeyManager(Class extends ContentItem> child, String childRelationshipPath) { 47 | mChild = child; 48 | mPath = childRelationshipPath; 49 | final DBSortOrder sortOrder = mChild.getAnnotation(DBSortOrder.class); 50 | mSortOrder = sortOrder != null ? sortOrder.value() : null; 51 | } 52 | 53 | /** 54 | * Unlike {@link #ForeignKeyManager(Class, String)}, the path is extracted from the 55 | * {@link UriPath} annotation on the child class. 56 | * 57 | * @param child 58 | * the child class that has this class as a foreign key 59 | */ 60 | public ForeignKeyManager(Class extends ContentItem> child) { 61 | mChild = child; 62 | 63 | mPath = UriPath.Extractor.extractUriPath(child, true); 64 | 65 | final DBSortOrder sortOrder = mChild.getAnnotation(DBSortOrder.class); 66 | mSortOrder = sortOrder != null ? sortOrder.value() : null; 67 | } 68 | 69 | public Uri getUri(Uri parent) { 70 | return Uri.withAppendedPath(parent, mPath); 71 | } 72 | 73 | /** 74 | * Retrieves the Uri of the specific child using this parent / relationship. 75 | * 76 | * @param parent 77 | * uri of the parent item 78 | * @param childId 79 | * the {@link BaseColumns#_ID} of the child 80 | * @return 81 | */ 82 | public Uri getUri(Uri parent, long childId) { 83 | return ContentUris.withAppendedId(getUri(parent), childId); 84 | } 85 | 86 | /** 87 | * Gets a URI that uses {@link ForeignKeyDBHelper#WILDCARD_PATH_SEGMENT WILDCARD_PATH_SEGMENT} 88 | * to retrieve all the children stored using the given {@link ForeignKeyDBHelper}. 89 | * 90 | * @param parentDir 91 | * the dir URI of the parent 92 | * @return a URI that will return all the children 93 | */ 94 | public Uri getAll(Uri parentDir) { 95 | return parentDir.buildUpon().appendPath(ForeignKeyDBHelper.WILDCARD_PATH_SEGMENT) 96 | .appendPath(mPath).build(); 97 | } 98 | 99 | public Uri insert(ContentResolver cr, Uri parent, ContentValues cv) { 100 | return cr.insert(getUri(parent), cv); 101 | } 102 | 103 | public Cursor query(ContentResolver cr, Uri parent, String[] projection) { 104 | return cr.query(getUri(parent), projection, null, null, mSortOrder); 105 | } 106 | 107 | public String getSortOrder() { 108 | return mSortOrder; 109 | } 110 | 111 | public String getPath() { 112 | return mPath; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/content/GenericDBHelper.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.content; 2 | /* 3 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 4 | * 5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License version 2.1 as published by the Free Software Foundation. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, visit 16 | * http://www.gnu.org/licenses/lgpl.html 17 | */ 18 | 19 | import android.content.ContentProvider; 20 | import android.content.ContentUris; 21 | import android.content.ContentValues; 22 | import android.database.Cursor; 23 | import android.database.SQLException; 24 | import android.database.sqlite.SQLiteDatabase; 25 | import android.net.Uri; 26 | import android.provider.BaseColumns; 27 | import edu.mit.mobile.android.content.annotation.SQLExtractor; 28 | import edu.mit.mobile.android.content.dbhelper.ContentItemDBHelper; 29 | 30 | /** 31 | * Provides basic CRUD database calls to handle very simple object types, eg: 32 | * 33 | *
34 | * content://AUTHORITY/item 35 | * content://AUTHORITY/item/1 36 | *37 | * 38 | * 39 | * @author Steve Pomeroy 40 | * 41 | */ 42 | public class GenericDBHelper extends ContentItemDBHelper { 43 | 44 | private final String mTable; 45 | private final SQLExtractor mExtractor; 46 | 47 | private boolean mCreatedTables = false; 48 | 49 | /** 50 | * @param contentItem 51 | * the class that defines the content item that will be managed by this helper. 52 | */ 53 | public GenericDBHelper(Class extends ContentItem> contentItem) { 54 | super(contentItem); 55 | mExtractor = new SQLExtractor(contentItem); 56 | mTable = mExtractor.getTableName(); 57 | } 58 | 59 | /** 60 | * This default implementation drops existing tables and recreates them. If you want to preserve 61 | * the user's data, please override this and handle migrations more carefully. 62 | * 63 | * @see edu.mit.mobile.android.content.DBHelper#upgradeTables(android.database.sqlite.SQLiteDatabase, 64 | * int, int) 65 | */ 66 | @Override 67 | public void upgradeTables(SQLiteDatabase db, int oldVersion, int newVersion) { 68 | db.execSQL("DROP TABLE IF EXISTS " + mTable); 69 | createTables(db); 70 | } 71 | 72 | public String getTable() { 73 | return mTable; 74 | } 75 | 76 | @Override 77 | public String getTargetTable() { 78 | return getTable(); 79 | } 80 | 81 | @Override 82 | public String getDirType(String authority, String path) { 83 | return ProviderUtils.toDirType(authority, mTable); 84 | } 85 | 86 | @Override 87 | public String getItemType(String authority, String path) { 88 | return ProviderUtils.toItemType(authority, mTable); 89 | } 90 | 91 | public Class extends ContentItem> getContentItem() { 92 | return mContentItem; 93 | } 94 | 95 | @Override 96 | public void createTables(SQLiteDatabase db) throws SQLGenerationException { 97 | if (mCreatedTables) { 98 | return; 99 | } 100 | for (final String sqlExpression : mExtractor.getTableCreation()) { 101 | db.execSQL(sqlExpression); 102 | } 103 | mCreatedTables = true; 104 | } 105 | 106 | protected ContentValues callOnPreSaveListener(SQLiteDatabase db, Uri uri, ContentValues values) { 107 | if (mOnSaveListener != null) { 108 | values = mOnSaveListener.onPreSave(db, null, values); 109 | } 110 | return values; 111 | } 112 | 113 | @Override 114 | public Uri insertDir(SQLiteDatabase db, ContentProvider provider, Uri uri, ContentValues values) 115 | throws SQLException { 116 | values = callOnPreSaveListener(db, uri, values); 117 | 118 | final long id = db.insertOrThrow(mTable, null, values); 119 | if (id != -1) { 120 | return ContentUris.withAppendedId(uri, id); 121 | } else { 122 | throw new SQLException("error inserting into " + mTable); 123 | } 124 | } 125 | 126 | @Override 127 | public int updateItem(SQLiteDatabase db, ContentProvider provider, Uri uri, 128 | ContentValues values, String where, String[] whereArgs) { 129 | 130 | values = callOnPreSaveListener(db, uri, values); 131 | 132 | return db.update(mTable, values, 133 | ProviderUtils.addExtraWhere(where, BaseColumns._ID + "=?"), 134 | ProviderUtils.addExtraWhereArgs(whereArgs, uri.getLastPathSegment())); 135 | } 136 | 137 | @Override 138 | public int updateDir(SQLiteDatabase db, ContentProvider provider, Uri uri, 139 | ContentValues values, String where, String[] whereArgs) { 140 | values = callOnPreSaveListener(db, uri, values); 141 | 142 | return db.update(mTable, values, where, whereArgs); 143 | } 144 | 145 | @Override 146 | public int deleteItem(SQLiteDatabase db, ContentProvider provider, Uri uri, String where, 147 | String[] whereArgs) { 148 | return db.delete(mTable, ProviderUtils.addExtraWhere(where, BaseColumns._ID + "=?"), 149 | ProviderUtils.addExtraWhereArgs(whereArgs, uri.getLastPathSegment())); 150 | } 151 | 152 | @Override 153 | public int deleteDir(SQLiteDatabase db, ContentProvider provider, Uri uri, String where, 154 | String[] whereArgs) { 155 | return db.delete(mTable, where, whereArgs); 156 | } 157 | 158 | @Override 159 | public Cursor queryDir(SQLiteDatabase db, Uri uri, String[] projection, String selection, 160 | String[] selectionArgs, String sortOrder) { 161 | 162 | return db.query(mTable, projection, selection, selectionArgs, null, null, 163 | sortOrder == null ? mSortOrder : sortOrder); 164 | 165 | } 166 | 167 | @Override 168 | public Cursor queryItem(SQLiteDatabase db, Uri uri, String[] projection, String selection, 169 | String[] selectionArgs, String sortOrder) { 170 | 171 | return db.query(mTable, projection, 172 | ProviderUtils.addExtraWhere(selection, BaseColumns._ID + "=?"), 173 | ProviderUtils.addExtraWhereArgs(selectionArgs, uri.getLastPathSegment()), null, 174 | null, sortOrder == null ? mSortOrder : sortOrder); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/content/Manager.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.content; 2 | /* 3 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 4 | * 5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License version 2.1 as published by the Free Software Foundation. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, visit 16 | * http://www.gnu.org/licenses/lgpl.html 17 | */ 18 | 19 | import android.content.ContentResolver; 20 | import android.content.ContentValues; 21 | import android.database.Cursor; 22 | import android.net.Uri; 23 | 24 | /** 25 | * Interface to provide common shortcuts for interacting with {@link ContentItem}s that relate to 26 | * one another. 27 | * 28 | */ 29 | public interface Manager { 30 | 31 | /** 32 | * @param parent 33 | * @return the URI of the list of items under this parent 34 | */ 35 | public abstract Uri getUri(Uri parent); 36 | 37 | /** 38 | * Adds an item to the destination 39 | * 40 | * @param cr 41 | * @param parent 42 | * @param cv 43 | * @return 44 | */ 45 | public abstract Uri insert(ContentResolver cr, Uri parent, ContentValues cv); 46 | 47 | public abstract Cursor query(ContentResolver cr, Uri parent, String[] projection); 48 | 49 | /** 50 | * @return the sort order of the given {@link ContentItem} 51 | */ 52 | public abstract String getSortOrder(); 53 | 54 | /** 55 | * @return the path segment used in URIs for this {@link ContentItem} 56 | */ 57 | public abstract String getPath(); 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/content/OnSaveListener.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.content; 2 | /* 3 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 4 | * 5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License version 2.1 as published by the Free Software Foundation. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, visit 16 | * http://www.gnu.org/licenses/lgpl.html 17 | */ 18 | 19 | import android.content.ContentValues; 20 | import android.database.sqlite.SQLiteDatabase; 21 | import android.net.Uri; 22 | 23 | /** 24 | * This hook runs right before insert or update, allowing the data to be modified before being saved 25 | * to the database. 26 | * 27 | * @author Steve Pomeroy 28 | * 29 | */ 30 | public interface OnSaveListener { 31 | 32 | /** 33 | * This hook runs right before insert or update, allowing the data to be modified before being 34 | * saved to the database. 35 | * 36 | * @param db 37 | * 38 | * @param uri 39 | * the uri of the item being updated or null if the item is being inserted. 40 | * 41 | * @param cv 42 | * the requested data to be updated or inserted. There is no guarantee that any of 43 | * the values will be present. 44 | * 45 | * @return the data that will be used for the save. This will usually be the same as the 46 | * passed-in cv. 47 | */ 48 | public ContentValues onPreSave(SQLiteDatabase db, Uri uri, ContentValues cv); 49 | } 50 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/content/ProviderUtils.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.content; 2 | /* 3 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 4 | * 5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License version 2.1 as published by the Free Software Foundation. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, visit 16 | * http://www.gnu.org/licenses/lgpl.html 17 | */ 18 | 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | import java.util.Vector; 23 | import java.util.regex.Pattern; 24 | 25 | import android.content.ContentValues; 26 | import android.database.Cursor; 27 | import android.net.Uri; 28 | import android.text.TextUtils; 29 | import android.util.Log; 30 | 31 | public class ProviderUtils { 32 | 33 | public static final String TYPE_DIR_PREFIX = "vnd.android.cursor.dir/vnd.", 34 | TYPE_ITEM_PREFIX = "vnd.android.cursor.item/vnd."; 35 | 36 | /** 37 | * Adds extra where clauses, encased in () and joined by AND. 38 | * 39 | * @param where 40 | * @param extraWhere 41 | * @return a query string with the extra clauses added in 42 | */ 43 | public static String addExtraWhere(String where, String... extraWhere) { 44 | // shortcut a common case 45 | if (where == null && extraWhere.length == 1) { 46 | return extraWhere[0]; 47 | } 48 | 49 | final String extraWhereJoined = "(" + TextUtils.join(") AND (", Arrays.asList(extraWhere)) 50 | + ")"; 51 | return extraWhereJoined 52 | + (where != null && where.length() > 0 ? " AND (" + where + ")" : ""); 53 | } 54 | 55 | /** 56 | * Adds in extra arguments to a where query. You'll have to put in the appropriate query 57 | * placeholders. 58 | * 59 | * @param whereArgs 60 | * the original whereArgs passed in from the query. Can be null. 61 | * @param extraArgs 62 | * Extra arguments needed for the query. 63 | * @return a new String[] with the extra arguments added in 64 | */ 65 | public static String[] addExtraWhereArgs(String[] whereArgs, String... extraArgs) { 66 | // shortcut a common case 67 | if (whereArgs == null) { 68 | return extraArgs; 69 | } 70 | 71 | final List
204 | * Generates a complete MIME type string in the following format: 205 | * {@code vnd.android.cursor.dir/vnd.AUTHORITY.SUFFIX} 206 | *
207 | * 208 | *209 | * SUFFIX is filtered so all invalid characters (see BCP13) are replaced with 211 | * {@link #MIME_INVALID_CHAR_REPLACEMENT}. 212 | *
213 | * 214 | * @param authority 215 | * the authority for this type 216 | * @param suffix 217 | * a raw suffix 218 | * @return the MIME type for the given suffix 219 | */ 220 | public static String toDirType(String authority, String suffix) { 221 | suffix = MIME_INVALID_CHARS.matcher(suffix).replaceAll(MIME_INVALID_CHAR_REPLACEMENT); 222 | return ProviderUtils.TYPE_DIR_PREFIX + authority + "." + suffix; 223 | } 224 | 225 | /** 226 | *227 | * Generates a complete MIME type string in the following format: 228 | * {@code vnd.android.cursor.item/vnd.AUTHORITY.SUFFIX} 229 | *
230 | * 231 | * 232 | *233 | * SUFFIX is filtered so all invalid characters (see BCP13) are replaced with 235 | * {@link #MIME_INVALID_CHAR_REPLACEMENT}. 236 | *
237 | * 238 | * @param authority 239 | * the authority for this type 240 | * @param suffix 241 | * a raw suffix 242 | * @return the MIME type for the given suffix 243 | */ 244 | public static String toItemType(String authority, String suffix) { 245 | suffix = MIME_INVALID_CHARS.matcher(suffix).replaceAll(MIME_INVALID_CHAR_REPLACEMENT); 246 | return ProviderUtils.TYPE_ITEM_PREFIX + authority + "." + suffix; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/content/SQLGenUtils.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.content; 2 | 3 | /* 4 | * Copyright (C) 2011 MIT Mobile Experience Lab 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | 21 | import java.util.Locale; 22 | import java.util.regex.Matcher; 23 | import java.util.regex.Pattern; 24 | 25 | /** 26 | * Helper functions for SQL generation. 27 | * 28 | * @author Steve Pomeroy 29 | * 30 | */ 31 | public class SQLGenUtils { 32 | 33 | private static final String VALID_NAME_STR = "([A-Za-z0-9_]+)"; 34 | // this pattern defines what a valid name (table name, column name, etc.) is in SQLite. 35 | private static final Pattern VALID_NAME = Pattern.compile(VALID_NAME_STR); 36 | // the inverse of the above pattern. 37 | private static final Pattern NON_NAME_CHARS = Pattern.compile("[^A-Za-z0-9_]+"); 38 | 39 | private static final Pattern VALID_QUALIFIED_COLUMN_NAME = Pattern.compile("(?:" 40 | + VALID_NAME_STR + "\\.)?" + VALID_NAME_STR); 41 | 42 | /** 43 | * Creates a valid SQLite name from the Java classname, lowercased. 44 | * 45 | * @param myClass 46 | * @return a valid SQL name 47 | */ 48 | public static final String toValidName(Class extends Object> myClass) { 49 | return toValidName(myClass.getSimpleName().toLowerCase(Locale.US)); 50 | } 51 | 52 | /** 53 | * Removes any non-name characters from the given name. 54 | * 55 | * @param name 56 | * @return a valid SQL name 57 | */ 58 | public static final String toValidName(String name) { 59 | // strip out any non-name characters from the name. 60 | final Matcher m = NON_NAME_CHARS.matcher(name); 61 | name = m.replaceAll(""); 62 | 63 | return name; 64 | } 65 | 66 | /** 67 | * @param name 68 | * @return true if the name is a valid SQLite name. 69 | */ 70 | public static boolean isValidName(String name) { 71 | return VALID_NAME.matcher(name).matches(); 72 | } 73 | 74 | /** 75 | * Unlike {@link #isValidName(String)}, this permits table prefixing of the supplied name. Eg. 76 | * {@code foo.bar}, and {@code bar} are both valid. 77 | * 78 | * @param column 79 | * @return 80 | */ 81 | public static boolean isValidQualifiedColumnName(String column) { 82 | return VALID_QUALIFIED_COLUMN_NAME.matcher(column).matches(); 83 | } 84 | 85 | /** 86 | * Escapes table names so they can be used in SQL queries. 87 | * 88 | * @param tableName 89 | * a plain table name 90 | * @return a quoted, escaped table name 91 | * @see http://stackoverflow.com/a/6701665/90934 93 | */ 94 | public static String escapeTableName(String tableName) { 95 | return '"' + tableName.replaceAll("\"", "\"\"") + '"'; 96 | 97 | } 98 | 99 | /** 100 | * Escapes a qualified column name, eg. {@code foo.bar} or {@code bar}. 101 | * 102 | * @param column 103 | * @return 104 | * @throws SQLGenerationException 105 | */ 106 | public static String escapeQualifiedColumn(String column) throws SQLGenerationException { 107 | if (!isValidQualifiedColumnName(column)) { 108 | throw new SQLGenerationException("column name is not valid"); 109 | } 110 | 111 | return VALID_NAME.matcher(column).replaceAll("\"$1\""); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/content/SQLGenerationException.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.content; 2 | /* 3 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 4 | * 5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License version 2.1 as published by the Free Software Foundation. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, visit 16 | * http://www.gnu.org/licenses/lgpl.html 17 | */ 18 | 19 | public class SQLGenerationException extends RuntimeException { 20 | /** 21 | * 22 | */ 23 | private static final long serialVersionUID = 1987806236697877222L; 24 | 25 | public SQLGenerationException() { 26 | super(); 27 | } 28 | 29 | public SQLGenerationException(String message) { 30 | super(message); 31 | } 32 | 33 | public SQLGenerationException(String message, Throwable initCause) { 34 | super(message); 35 | initCause(initCause); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/content/UriPath.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.content; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * Optional. For use on {@link ContentItem} classes. This specifies the path under which the content 11 | * items are found so that helpers like {@link ForeignKeyManager} can automatically construct URLs. 12 | * 13 | */ 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.TYPE) 16 | @Documented 17 | public @interface UriPath { 18 | 19 | public String value(); 20 | 21 | public static class Extractor { 22 | /** 23 | * Extract the UriPath value from the given class. 24 | * 25 | * @param contentItem 26 | * @param required 27 | * @return the path as specified by the UriPath annotation or null. 28 | * @throws SQLGenerationException 29 | * if required is true and the annotation is missing. 30 | * 31 | */ 32 | public static String extractUriPath(Class extends ContentItem> contentItem, 33 | boolean required) { 34 | String pathString; 35 | final UriPath path = contentItem.getAnnotation(UriPath.class); 36 | pathString = path != null ? path.value() : null; 37 | if (required && pathString == null) { 38 | throw new SQLGenerationException("ForeignKeyManager: missing @UriPath on " 39 | + contentItem); 40 | } 41 | return pathString; 42 | } 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/content/column/BlobColumn.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.content.column; 2 | /* 3 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 4 | * 5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License version 2.1 as published by the Free Software Foundation. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, visit 16 | * http://www.gnu.org/licenses/lgpl.html 17 | */ 18 | 19 | import android.database.Cursor; 20 | 21 | public class BlobColumn extends DBColumnType36 | * @DBColumn(type=IntegerColumn.class) 37 | * final static String MY_COL = "my_col" 38 | *39 | * 40 | * The names and structure of this are based loosely on Django's Model/Field framework. 41 | * 42 | * @author Steve Pomeroy 43 | * 44 | */ 45 | @Retention(RetentionPolicy.RUNTIME) 46 | @Target(ElementType.FIELD) 47 | @Documented 48 | public @interface DBColumn { 49 | 50 | // this is required because Java doesn't allow null as a default value. 51 | // For some reason, null is not considered a constant expression. 52 | public static final String NULL = "██████NULL██████"; 53 | 54 | public static final long NULL_LONG = Long.MIN_VALUE; 55 | public static final int NULL_INT = Integer.MIN_VALUE; 56 | public static final float NULL_FLOAT = Float.MIN_VALUE; 57 | public static final double NULL_DOUBLE = Double.MIN_VALUE; 58 | 59 | /** 60 | * Specify one of the column types by passing its class object. 61 | * 62 | * 63 | * @see IntegerColumn 64 | * @see TextColumn 65 | * @see TimestampColumn 66 | * @see DatetimeColumn 67 | * 68 | * @return the column type 69 | */ 70 | Class extends DBColumnType>> type(); 71 | 72 | /** 73 | * Sets this column to be NOT NULL. 74 | * 75 | * @return true if the column is NOT NULL 76 | */ 77 | boolean notnull() default false; 78 | 79 | /** 80 | * Sets this column to be a PRIMARY KEY. 81 | * 82 | * @return true if the column is a primary key 83 | */ 84 | boolean primaryKey() default false; 85 | 86 | /** 87 | * Adds the AUTOINCREMENT flag if {@link #primaryKey()} has also been set. 88 | * 89 | * @return true if this column should be auto-incremented 90 | */ 91 | boolean autoIncrement() default false; 92 | 93 | /** 94 | * Sets a default value for the column. Values are automatically quoted as strings in SQL. To 95 | * avoid escaping (for use with reserved words and such), prefix with 96 | * {@link DBColumnType#DEFAULT_VALUE_ESCAPE}. 97 | * 98 | * @return the default value 99 | */ 100 | String defaultValue() default NULL; 101 | 102 | /** 103 | * Sets the default value for the column. 104 | * 105 | * @return the default value 106 | */ 107 | int defaultValueInt() default NULL_INT; 108 | 109 | /** 110 | * Sets the default value for the column. 111 | * 112 | * @return the default value 113 | */ 114 | long defaultValueLong() default NULL_LONG; 115 | 116 | /** 117 | * Sets the default value for the column. 118 | * 119 | * @return the default value 120 | */ 121 | float defaultValueFloat() default NULL_FLOAT; 122 | 123 | /** 124 | * Sets the default value for the column. 125 | * 126 | * @return the default value 127 | */ 128 | double defaultValueDouble() default NULL_DOUBLE; 129 | 130 | /** 131 | * If true, ensures that this column is unique. 132 | * 133 | * @return true if this column is UNIQUE 134 | */ 135 | boolean unique() default false; 136 | 137 | public static enum OnConflict { 138 | UNSPECIFIED, ROLLBACK, ABORT, FAIL, IGNORE, REPLACE 139 | } 140 | 141 | /** 142 | * If the column is marked unique, this determines what to do if there's a conflict. This is 143 | * ignored if the column is not unique. 144 | * 145 | * @return the desired conflict resolution 146 | */ 147 | OnConflict onConflict() default OnConflict.UNSPECIFIED; 148 | 149 | public static enum CollationName { 150 | DEFAULT, BINARY, NOCASE, RTRIM 151 | } 152 | 153 | /** 154 | * Defines a collation for the column. 155 | * 156 | * @return the collation type 157 | */ 158 | CollationName collate() default CollationName.DEFAULT; 159 | 160 | /** 161 | * Suffixes the column declaration with this string. 162 | * 163 | * @return a string of any supplemental column declarations 164 | */ 165 | String extraColDef() default NULL; 166 | 167 | /** 168 | * Column type-specific flags. 169 | * 170 | * @return 171 | */ 172 | int flags() default 0; 173 | } 174 | -------------------------------------------------------------------------------- /src/edu/mit/mobile/android/content/column/DBColumnType.java: -------------------------------------------------------------------------------- 1 | package edu.mit.mobile.android.content.column; 2 | /* 3 | * Copyright (C) 2011-2013 MIT Mobile Experience Lab 4 | * 5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License version 2.1 as published by the Free Software Foundation. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, visit 16 | * http://www.gnu.org/licenses/lgpl.html 17 | */ 18 | 19 | import android.database.Cursor; 20 | 21 | public abstract class DBColumnType
See {@link edu.mit.mobile.android.content.SimpleContentProvider} for an example of how to use this package.
7 | 8 |