Java tutorial
/******************************************************************************* * This file is part of Zandy. * * Zandy is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Zandy is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Zandy. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package com.gimranov.zandy.app.task; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteStatement; import android.os.Handler; import android.util.Log; import com.gimranov.zandy.app.ServerCredentials; import com.gimranov.zandy.app.XMLResponseParser; import com.gimranov.zandy.app.data.Attachment; import com.gimranov.zandy.app.data.Database; import com.gimranov.zandy.app.data.Item; import com.gimranov.zandy.app.data.ItemCollection; import org.apache.http.HttpEntity; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.DefaultHttpClient; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.UUID; /** * Represents a request to the Zotero API. These can be consumed by * other things like ZoteroAPITask. These should be queued up for many purposes. * * The APIRequest should include the HttpPost / HttpGet / etc. that it needs * to be executed, and optionally a callback to be called when it completes. * * See http://www.zotero.org/support/dev/server_api for information. * * @author ajlyon * */ public class APIRequest { private static final String TAG = "com.gimranov.zandy.app.task.APIRequest"; /** * Statuses used for items and collections. They are currently strings, but * they should change to integers. These statuses may be stored in the database. */ // XXX i18n public static final String API_DIRTY = "Unsynced change"; public static final String API_NEW = "New item / collection"; public static final String API_MISSING = "Partial data"; public static final String API_STALE = "Stale data"; public static final String API_WIP = "Sync attempted"; public static final String API_CLEAN = "No unsynced change"; /** * These are constants represented by integers. * * The above should be moving down here some time. */ /** * HTTP response codes that we are used to */ public static final int HTTP_ERROR_CONFLICT = 412; public static final int HTTP_ERROR_UNSPECIFIED = 400; /** * The following are used when passing things back to the UI * from the API request service / thread. */ /** Used to indicate database data has changed. */ public static final int UPDATED_DATA = 1000; /** Current set of requests completed. */ public static final int BATCH_DONE = 2000; /** Used to indicate an error with no more details. */ public static final int ERROR_UNKNOWN = 4000; /** Queued more requests */ public static final int QUEUED_MORE = 3000; /** * Request types */ public static final int ITEMS_ALL = 10000; public static final int ITEMS_FOR_COLLECTION = 10001; public static final int ITEMS_CHILDREN = 10002; public static final int COLLECTIONS_ALL = 10003; public static final int ITEM_BY_KEY = 10004; // Requests that require write access public static final int ITEM_NEW = 20000; public static final int ITEM_UPDATE = 20001; public static final int ITEM_DELETE = 20002; public static final int ITEM_MEMBERSHIP_ADD = 20003; public static final int ITEM_MEMBERSHIP_REMOVE = 20004; public static final int ITEM_ATTACHMENT_NEW = 20005; public static final int ITEM_ATTACHMENT_UPDATE = 20006; public static final int ITEM_ATTACHMENT_DELETE = 20007; public static final int ITEM_FIELDS = 30000; public static final int CREATOR_TYPES = 30001; public static final int ITEM_FIELDS_L10N = 30002; public static final int CREATOR_TYPES_L10N = 30003; /** * Request status for use within the database */ public static final int REQ_NEW = 40000; public static final int REQ_FAILING = 41000; /** * We'll request the whole collection or library rather than * individual feeds when we have less than this proportion of * the items. Used when pre-fetching keys. */ public static double REREQUEST_CUTOFF = 0.7; /** * Type of request we're sending. This should be one of * the request types listed above. */ public int type; /** * Callback handler */ private APIEvent handler; /** * Base query to send. */ public String query; /** * API key used to make request. Can be omitted for requests that don't need one. */ public String key; /** * One of get, put, post, delete. * Lower-case preferred, but we coerce them anyway. */ public String method; /** * Response disposition: xml or raw. JSON also planned */ public String disposition; /** * Used when sending JSON in POST and PUT requests. */ public String contentType = "application/json"; /** * Optional token to avoid accidentally sending one request twice. The * server will decline to carry out a second request with the same writeToken * for a single API key in a several-hour period. */ public String writeToken; /** * The eTag received from the server when requesting an item. We can make changes * (delete, update) to an item only while the tag is valid; if the item changes * server-side, our request will be declined until we request the item anew and get * a new valid eTag. */ public String ifMatch; /** * Request body, generally JSON. */ public String body; /** * The temporary key (UUID) that the request is based on. */ public String updateKey; /** * Type of object we expect to get. This and the updateKey are used to update * the UUIDs / local keys of locally-created items. I know, it's a hack. */ public String updateType; /** * Status code for the request. Codes should be constants defined in APIRequest; * take the REQ_* code and add the response code if applicable. */ public int status; /** * UUID for this request. We use this for DB lookups and as the write token when * appropriate. Every request should have one. */ private String uuid; /** * Timestamp when this request was first created. */ private Date created; /** * Timestamp when this request was last attempted to be run. */ private Date lastAttempt; /** * Creates a basic APIRequest item. Augment the item using instance methods for more complex * requests, or pass it to ZoteroAPITask for simpler ones. The request can be run by * simply calling the instance method `issue(..)`, but not from the UI thread. * * The constructor is not to be used directly; use the static methods in this class, or create from * a cursor. The one exception is the Atom feed continuations produced by XMLResponseParser, but that * should be moved into this class as well. * * @param query Fragment being requested, like /items * @param method GET, POST, PUT, or DELETE (except that lowercase is preferred) * @param key Can be null, if you're planning on making requests that don't need a key. */ public APIRequest(String query, String method, String key) { this.query = query; this.method = method; this.key = key; // default to XML processing this.disposition = "xml"; // If this is processing-intensive, we can probably move it to the save method this.uuid = UUID.randomUUID().toString(); created = new Date(); } /** * Load an APIRequest from its serialized form in the database * public static final String[] REQUESTCOLS = {"_id", "uuid", "type", "query", "key", "method", "disposition", "if_match", "update_key", "update_type", "created", "last_attempt", "status"}; * @param uuid */ public APIRequest(Cursor cur) { // N.B.: getString and such use 0-based indexing this.uuid = cur.getString(1); this.type = cur.getInt(2); this.query = cur.getString(3); this.key = cur.getString(4); this.method = cur.getString(5); this.disposition = cur.getString(6); this.ifMatch = cur.getString(7); this.updateKey = cur.getString(8); this.updateType = cur.getString(9); this.created = new Date(); this.created.setTime(cur.getLong(10)); this.lastAttempt = new Date(); this.lastAttempt.setTime(cur.getLong(11)); this.status = cur.getInt(12); this.body = cur.getString(13); } /** * Saves the APIRequest's basic info to the database. Does not maintain handler information. * @param db */ public void save(Database db) { try { Log.d(TAG, "Saving APIRequest to database: " + uuid + " " + query); SQLiteStatement insert = db.compileStatement("insert or replace into apirequests " + "(uuid, type, query, key, method, disposition, if_match, update_key, update_type, " + "created, last_attempt, status, body)" + " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?)"); // Why, oh why does bind* use 1-based indexing? And cur.get* uses 0-based! insert.bindString(1, uuid); insert.bindLong(2, (long) type); String createdUnix = Long.toString(created.getTime()); String lastAttemptUnix; if (lastAttempt == null) lastAttemptUnix = null; else lastAttemptUnix = Long.toString(lastAttempt.getTime()); String status = Integer.toString(this.status); // Iterate through null-allowed strings and bind them String[] strings = { query, key, method, disposition, ifMatch, updateKey, updateType, createdUnix, lastAttemptUnix, status, body }; for (int i = 0; i < strings.length; i++) { Log.d(TAG, (3 + i) + ":" + strings[i]); if (strings[i] == null) insert.bindNull(3 + i); else insert.bindString(3 + i, strings[i]); } insert.executeInsert(); insert.clearBindings(); insert.close(); } catch (SQLiteException e) { Log.e(TAG, "Exception compiling or running insert statement", e); throw e; } } /** * Getter for the request's implementation of the APIEvent interface, * used for call-backs, usually tying into the UI. * * Returns a no-op, logging handler if none specified * * @return */ public APIEvent getHandler() { if (handler == null) { /* * We have to fall back on a no-op event handler to prevent null exceptions */ return new APIEvent() { @Override public void onComplete(APIRequest request) { Log.d(TAG, "onComplete called but no handler"); } @Override public void onUpdate(APIRequest request) { Log.d(TAG, "onUpdate called but no handler"); } @Override public void onError(APIRequest request, Exception exception) { Log.d(TAG, "onError called but no handler"); } @Override public void onError(APIRequest request, int error) { Log.d(TAG, "onError called but no handler"); } }; } return handler; } public void setHandler(APIEvent handler) { if (this.handler == null) { this.handler = handler; return; } Log.e(TAG, "APIEvent handler for request cannot be replaced"); } /** * Set an Android standard handler to be used for the APIEvents * @param handler */ public void setHandler(Handler handler) { final Handler mHandler = handler; if (this.handler == null) { this.handler = new APIEvent() { @Override public void onComplete(APIRequest request) { mHandler.sendEmptyMessage(BATCH_DONE); } @Override public void onUpdate(APIRequest request) { mHandler.sendEmptyMessage(UPDATED_DATA); } @Override public void onError(APIRequest request, Exception exception) { mHandler.sendEmptyMessage(ERROR_UNKNOWN); } @Override public void onError(APIRequest request, int error) { mHandler.sendEmptyMessage(ERROR_UNKNOWN); } }; return; } Log.e(TAG, "APIEvent handler for request cannot be replaced"); } /** * Populates the body with a JSON representation of * the specified item. * * Use this for updating items, i.e.: * PUT /users/1/items/ABCD2345 * @param item Item to put in the body. */ public void setBody(Item item) { try { body = item.getContent().toString(4); } catch (JSONException e) { Log.e(TAG, "Error setting body for item", e); } } /** * Populates the body with a JSON representation of the specified * items. * * Use this for creating new items, i.e.: * POST /users/1/items * @param items */ public void setBody(ArrayList<Item> items) { try { JSONArray array = new JSONArray(); for (Item i : items) { JSONObject jItem = i.getContent(); array.put(jItem); } JSONObject obj = new JSONObject(); obj.put("items", array); body = obj.toString(4); } catch (JSONException e) { Log.e(TAG, "Error setting body for items", e); } } /** * Populates the body with a JSON representation of specified * attachments; note that this is will not work with non-note * attachments until the server API supports them. * * @param attachments */ public void setBodyWithNotes(ArrayList<Attachment> attachments) { try { JSONArray array = new JSONArray(); for (Attachment a : attachments) { JSONObject jAtt = a.content; array.put(jAtt); } JSONObject obj = new JSONObject(); obj.put("items", array); body = obj.toString(4); } catch (JSONException e) { Log.e(TAG, "Error setting body for attachments", e); } } /** * Getter for the request's UUID * @return */ public String getUuid() { return uuid; } /** * Sets the HTTP response code portion of the request's status * * @param code * @return The new status */ public int setHttpStatus(int code) { status = (status - status % 1000) + code; return status; } /** * Gets the HTTP response code portion of the request's status; * returns 0 if there was no code set. */ public int getHttpStatus() { return status % 1000; } /** * Record a failed attempt to run the request. * * Saves the APIRequest in its current state. * * @param db Database object * @return Date object with new lastAttempt value */ public Date recordAttempt(Database db) { lastAttempt = new Date(); save(db); return lastAttempt; } /** * To be called when the request succeeds. Currently just * deletes the corresponding row from the database. * * @param db Database object */ public void succeeded(Database db) { getHandler().onComplete(this); String[] args = { uuid }; db.rawQuery("delete from apirequests where uuid=?", args); } /** * Returns HTML-formatted string of the request * * XXX i18n, once we settle on a format * * @return */ public String toHtmlString() { StringBuilder sb = new StringBuilder(); sb.append("<h1>"); sb.append(status); sb.append("</h1>"); sb.append("<p><i>"); sb.append(method + "</i> " + query); sb.append("</p>"); sb.append("<p>Body: "); sb.append(body); sb.append("</p>"); sb.append("<p>Created: "); sb.append(created.toString()); sb.append("</p>"); sb.append("<p>Attempted: "); if (lastAttempt.getTime() == 0) sb.append("Never"); else sb.append(lastAttempt.toString()); sb.append("</p>"); return sb.toString(); } /** * Issues the specified request, calling its specified handler as appropriate * * This should not be run from a UI thread * * @return * @throws APIException */ public void issue(Database db, ServerCredentials cred) throws APIException { URI uri; // Add the API key, if missing and we have it if (!query.contains("key=") && key != null) { String suffix = (query.contains("?")) ? "&key=" + key : "?key=" + key; query = query + suffix; } // Force lower-case method = method.toLowerCase(); Log.i(TAG, "Request " + method + ": " + query); try { uri = new URI(query); } catch (URISyntaxException e1) { throw new APIException(APIException.INVALID_URI, "Invalid URI: " + query, this); } HttpClient client = new DefaultHttpClient(); // The default implementation includes an Expect: header, which // confuses the Zotero servers. client.getParams().setParameter("http.protocol.expect-continue", false); // We also need to send our data nice and raw. client.getParams().setParameter("http.protocol.content-charset", "UTF-8"); HttpGet get = new HttpGet(uri); HttpPost post = new HttpPost(uri); HttpPut put = new HttpPut(uri); HttpDelete delete = new HttpDelete(uri); for (HttpRequest request : Arrays.asList(get, post, put, delete)) { request.setHeader("Zotero-API-Version", "1"); } // There are several shared initialization routines for POST and PUT if ("post".equals(method) || "put".equals(method)) { if (ifMatch != null) { post.setHeader("If-Match", ifMatch); put.setHeader("If-Match", ifMatch); } if (contentType != null) { post.setHeader("Content-Type", contentType); put.setHeader("Content-Type", contentType); } if (body != null) { Log.d(TAG, "Request body: " + body); // Force the encoding to UTF-8 StringEntity entity; try { entity = new StringEntity(body, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new APIException(APIException.INVALID_UUID, "UnsupportedEncodingException. This shouldn't " + "be possible-- UTF-8 is certainly supported", this); } post.setEntity(entity); put.setEntity(entity); } } if ("get".equals(method)) { if (contentType != null) { get.setHeader("Content-Type", contentType); } } /* For requests that return Atom feeds or entries (XML): * ITEMS_ALL ] * ITEMS_FOR_COLLECTION ]- Except format=keys * ITEMS_CHILDREN ] * * ITEM_BY_KEY * COLLECTIONS_ALL * ITEM_NEW * ITEM_UPDATE * ITEM_ATTACHMENT_NEW * ITEM_ATTACHMENT_UPDATE */ if ("xml".equals(disposition)) { XMLResponseParser parse = new XMLResponseParser(this); // These types will always have a temporary key that we've // been using locally, and which should be replaced by the // incoming item key. if (type == ITEM_NEW || type == ITEM_ATTACHMENT_NEW) { parse.update(updateType, updateKey); } try { HttpResponse hr; if ("post".equals(method)) { hr = client.execute(post); } else if ("put".equals(method)) { hr = client.execute(put); } else { // We fall back on GET here, but there really // shouldn't be anything else, so we throw in that case // for good measure if (!"get".equals(method)) { throw new APIException(APIException.INVALID_METHOD, "Unexpected method: " + method, this); } hr = client.execute(get); } // Record the response code status = hr.getStatusLine().getStatusCode(); Log.d(TAG, status + " : " + hr.getStatusLine().getReasonPhrase()); if (status < 400) { HttpEntity he = hr.getEntity(); InputStream in = he.getContent(); parse.setInputStream(in); // Entry mode if the request is an update (PUT) or if it is a request // for a single item by key (ITEM_BY_KEY) int mode = ("put".equals(method) || type == APIRequest.ITEM_BY_KEY) ? XMLResponseParser.MODE_ENTRY : XMLResponseParser.MODE_FEED; try { parse.parse(mode, uri.toString(), db); } catch (RuntimeException e) { throw new RuntimeException("Parser threw exception on request: " + method + " " + query, e); } } else { ByteArrayOutputStream ostream = new ByteArrayOutputStream(); hr.getEntity().writeTo(ostream); Log.e(TAG, "Error Body: " + ostream.toString()); Log.e(TAG, "Request Body:" + body); if (status == 412) { // This is: "Precondition Failed", meaning that we provided // the wrong etag to update the item. That should mean that // there is a conflict between what we're sending (PUT) and // the server. We mark that ourselves and save the request // to the database, and also notify our handler. getHandler().onError(this, APIRequest.HTTP_ERROR_CONFLICT); } else { Log.e(TAG, "Response status " + status + " : " + ostream.toString()); getHandler().onError(this, APIRequest.HTTP_ERROR_UNSPECIFIED); } status = getHttpStatus() + REQ_FAILING; recordAttempt(db); // I'm not sure whether we should throw here throw new APIException(APIException.HTTP_ERROR, ostream.toString(), this); } } catch (Exception e) { StringBuilder sb = new StringBuilder(); for (StackTraceElement el : e.getStackTrace()) { sb.append(el.toString() + "\n"); } recordAttempt(db); throw new APIException(APIException.HTTP_ERROR, "An IOException was thrown: " + sb.toString(), this); } } // end if ("xml".equals(disposition)) {..} /* For requests that return non-XML data: * ITEMS_ALL ] * ITEMS_FOR_COLLECTION ]- For format=keys * ITEMS_CHILDREN ] * * No server response: * ITEM_DELETE * ITEM_MEMBERSHIP_ADD * ITEM_MEMBERSHIP_REMOVE * ITEM_ATTACHMENT_DELETE * * Currently not supported; return JSON: * ITEM_FIELDS * CREATOR_TYPES * ITEM_FIELDS_L10N * CREATOR_TYPES_L10N * * These ones use BasicResponseHandler, which gives us * the response as a basic string. This is only appropriate * for smaller responses, since it means we have to wait until * the entire response is received before parsing it, so we * don't use it for the XML responses. * * The disposition here is "none" or "raw". * * The JSON-returning requests, such as ITEM_FIELDS, are not currently * supported; they should have a disposition of their own. */ else { BasicResponseHandler brh = new BasicResponseHandler(); String resp; try { if ("post".equals(method)) { resp = client.execute(post, brh); } else if ("put".equals(method)) { resp = client.execute(put, brh); } else if ("delete".equals(method)) { resp = client.execute(delete, brh); } else { // We fall back on GET here, but there really // shouldn't be anything else, so we throw in that case // for good measure if (!"get".equals(method)) { throw new APIException(APIException.INVALID_METHOD, "Unexpected method: " + method, this); } resp = client.execute(get, brh); } } catch (IOException e) { StringBuilder sb = new StringBuilder(); for (StackTraceElement el : e.getStackTrace()) { sb.append(el.toString() + "\n"); } recordAttempt(db); throw new APIException(APIException.HTTP_ERROR, "An IOException was thrown: " + sb.toString(), this); } if ("raw".equals(disposition)) { /* * The output should be a newline-delimited set of alphanumeric * keys. */ String[] keys = resp.split("\n"); ArrayList<String> missing = new ArrayList<String>(); if (type == ITEMS_ALL || type == ITEMS_FOR_COLLECTION) { // Try to get a parent collection // Our query looks like this: // /users/5770/collections/2AJUSIU9/items int colloc = query.indexOf("/collections/"); int itemloc = query.indexOf("/items"); // The string "/collections/" is thirteen characters long ItemCollection coll = ItemCollection.load(query.substring(colloc + 13, itemloc), db); if (coll != null) { coll.loadChildren(db); // If this is a collection's key listing, we first look // for any synced keys we have that aren't in the list ArrayList<String> keyAL = new ArrayList<String>(Arrays.asList(keys)); ArrayList<Item> notThere = coll.notInKeys(keyAL); // We should then remove those memberships for (Item i : notThere) { coll.remove(i, true, db); } } ArrayList<Item> recd = new ArrayList<Item>(); for (int j = 0; j < keys.length; j++) { Item got = Item.load(keys[j], db); if (got == null) { missing.add(keys[j]); } else { // We can update the collection membership immediately if (coll != null) coll.add(got, true, db); recd.add(got); } } if (coll != null) { coll.saveChildren(db); coll.save(db); } Log.d(TAG, "Received " + keys.length + " keys, " + missing.size() + " missing ones"); Log.d(TAG, "Have " + (double) recd.size() / keys.length + " of list"); if (recd.size() == keys.length) { Log.d(TAG, "No new items"); succeeded(db); } else if ((double) recd.size() / keys.length < REREQUEST_CUTOFF) { Log.d(TAG, "Requesting full list"); APIRequest mReq; if (type == ITEMS_FOR_COLLECTION) { mReq = fetchItems(coll, false, cred); } else { mReq = fetchItems(false, cred); } mReq.status = REQ_NEW; mReq.save(db); } else { Log.d(TAG, "Requesting " + missing.size() + " items one by one"); APIRequest mReq; for (String key : missing) { // Queue request for the missing key mReq = fetchItem(key, cred); mReq.status = REQ_NEW; mReq.save(db); } // Queue request for the collection again, by key // XXX This is not the best way to make sure these // items are put in the correct collection. if (type == ITEMS_FOR_COLLECTION) { fetchItems(coll, true, cred).save(db); } } } else if (type == ITEMS_CHILDREN) { // Try to get a parent item // Our query looks like this: // /users/5770/items/2AJUSIU9/children int itemloc = query.indexOf("/items/"); int childloc = query.indexOf("/children"); // The string "/items/" is seven characters long Item item = Item.load(query.substring(itemloc + 7, childloc), db); ArrayList<Attachment> recd = new ArrayList<Attachment>(); for (int j = 0; j < keys.length; j++) { Attachment got = Attachment.load(keys[j], db); if (got == null) missing.add(keys[j]); else recd.add(got); } if ((double) recd.size() / keys.length < REREQUEST_CUTOFF) { APIRequest mReq; mReq = cred.prep(children(item)); mReq.status = REQ_NEW; mReq.save(db); } else { APIRequest mReq; for (String key : missing) { // Queue request for the missing key mReq = fetchItem(key, cred); mReq.status = REQ_NEW; mReq.save(db); } } } } else if ("json".equals(disposition)) { // TODO } else { /* Here, disposition should be "none" */ // Nothing to be done. } getHandler().onComplete(this); } } /** NEXT SECTION: Static methods for generating APIRequests */ /** * Produces an API request for the specified item key * * @param key Item key * @param cred Credentials */ public static APIRequest fetchItem(String key, ServerCredentials cred) { APIRequest req = new APIRequest(ServerCredentials.APIBASE + cred.prep(ServerCredentials.ITEMS) + "/" + key, "get", null); req.query = req.query + "?content=json"; req.disposition = "xml"; req.type = ITEM_BY_KEY; req.key = cred.getKey(); return req; } /** * Produces an API request for the items in a specified collection. * * @param collection The collection to fetch * @param keysOnly Use format=keys rather than format=atom/content=json * @param cred Credentials */ public static APIRequest fetchItems(ItemCollection collection, boolean keysOnly, ServerCredentials cred) { return fetchItems(collection.getKey(), keysOnly, cred); } /** * Produces an API request for the items in a specified collection. * * @param collectionKey The collection to fetch * @param keysOnly Use format=keys rather than format=atom/content=json * @param cred Credentials */ public static APIRequest fetchItems(String collectionKey, boolean keysOnly, ServerCredentials cred) { APIRequest req = new APIRequest(ServerCredentials.APIBASE + cred.prep(ServerCredentials.COLLECTIONS) + "/" + collectionKey + "/items", "get", null); if (keysOnly) { req.query = req.query + "?format=keys"; req.disposition = "raw"; } else { req.query = req.query + "?content=json"; req.disposition = "xml"; } req.type = APIRequest.ITEMS_FOR_COLLECTION; req.key = cred.getKey(); return req; } /** * Produces an API request for all items * * @param keysOnly Use format=keys rather than format=atom/content=json * @param cred Credentials */ public static APIRequest fetchItems(boolean keysOnly, ServerCredentials cred) { APIRequest req = new APIRequest(ServerCredentials.APIBASE + cred.prep(ServerCredentials.ITEMS) + "/top", "get", null); if (keysOnly) { req.query = req.query + "?format=keys"; req.disposition = "raw"; } else { req.query = req.query + "?content=json"; req.disposition = "xml"; } req.type = APIRequest.ITEMS_ALL; req.key = cred.getKey(); return req; } /** * Produces an API request for all collections * * @param c Context */ public static APIRequest fetchCollections(ServerCredentials cred) { APIRequest req = new APIRequest( ServerCredentials.APIBASE + cred.prep(ServerCredentials.COLLECTIONS) + "?content=json", "get", null); req.disposition = "xml"; req.type = APIRequest.COLLECTIONS_ALL; req.key = cred.getKey(); return req; } /** * Produces an API request to remove the specified item from the collection. * This request always needs a key, but it isn't set automatically and should * be set by whatever consumes this request. * * From the API docs: * DELETE /users/1/collections/QRST9876/items/ABCD2345 * * @param item * @param collection * @return */ public static APIRequest remove(Item item, ItemCollection collection) { APIRequest templ = new APIRequest(ServerCredentials.APIBASE + ServerCredentials.COLLECTIONS + "/" + collection.getKey() + "/items/" + item.getKey(), "DELETE", null); templ.disposition = "none"; return templ; } /** * Produces an API request to add the specified items to the collection. * This request always needs a key, but it isn't set automatically and should * be set by whatever consumes this request. * * From the API docs: * POST /users/1/collections/QRST9876/items * * ABCD2345 FBCD2335 * * @param items * @param collection * @return */ public static APIRequest add(ArrayList<Item> items, ItemCollection collection) { APIRequest templ = new APIRequest( ServerCredentials.APIBASE + ServerCredentials.COLLECTIONS + "/" + collection.getKey() + "/items", "POST", null); templ.body = ""; for (Item i : items) { templ.body += i.getKey() + " "; } templ.disposition = "none"; return templ; } /** * Craft a request to add a single item to the server * * @param item * @param collection * @return */ public static APIRequest add(Item item, ItemCollection collection) { ArrayList<Item> items = new ArrayList<Item>(); items.add(item); return add(items, collection); } /** * Craft a request to add items to the server * This does not attempt to update them, just add them. * * @param items * @return */ public static APIRequest add(ArrayList<Item> items) { APIRequest templ = new APIRequest(ServerCredentials.APIBASE + ServerCredentials.ITEMS + "?content=json", "POST", null); templ.setBody(items); templ.disposition = "xml"; templ.updateType = "item"; // TODO this needs to be reworked to send all the keys. Or the whole system // needs to be reworked. Log.d(TAG, "Using the templ key of the first new item for now..."); templ.updateKey = items.get(0).getKey(); return templ; } /** * Craft a request to add child items (notes, attachments) to the server * This does not attempt to update them, just add them. * * @param item The parent item of the attachments * @param attachments * @return */ public static APIRequest add(Item item, ArrayList<Attachment> attachments) { APIRequest templ = new APIRequest(ServerCredentials.APIBASE + ServerCredentials.ITEMS + "/" + item.getKey() + "/children?content=json", "POST", null); templ.setBodyWithNotes(attachments); templ.disposition = "xml"; templ.updateType = "attachment"; // TODO this needs to be reworked to send all the keys. Or the whole system // needs to be reworked. Log.d(TAG, "Using the templ key of the first new attachment for now..."); templ.updateKey = attachments.get(0).key; return templ; } /** * Craft a request for the children of the specified item * @param item * @return */ public static APIRequest children(Item item) { APIRequest templ = new APIRequest(ServerCredentials.APIBASE + ServerCredentials.ITEMS + "/" + item.getKey() + "/children?content=json", "GET", null); templ.disposition = "xml"; templ.type = ITEMS_CHILDREN; return templ; } /** * Craft a request to update an attachment on the server * Does not refresh eTag * * @param attachment * @return */ public static APIRequest update(Attachment attachment, Database db) { Log.d(TAG, "Attachment key pre-update: " + attachment.key); // If we have an attachment marked as new, update it if (attachment.key.length() > 10) { Item item = Item.load(attachment.parentKey, db); ArrayList<Attachment> aL = new ArrayList<Attachment>(); aL.add(attachment); if (item == null) { Log.e(TAG, "Orphaned attachment with key: " + attachment.key); attachment.delete(db); // send something, so we don't get errors elsewhere return new APIRequest(ServerCredentials.APIBASE, "GET", null); } return add(item, aL); } APIRequest templ = new APIRequest( ServerCredentials.APIBASE + ServerCredentials.ITEMS + "/" + attachment.key, "PUT", null); try { templ.body = attachment.content.toString(4); } catch (JSONException e) { Log.e(TAG, "JSON exception setting body for attachment update: " + attachment.key, e); } templ.ifMatch = '"' + attachment.etag + '"'; templ.disposition = "xml"; return templ; } /** * Craft a request to update an attachment on the server * Does not refresh eTag * * @param item * @return */ public static APIRequest update(Item item) { // If we have an item markes as new, update it if (item.getKey().length() > 10) { ArrayList<Item> mAL = new ArrayList<Item>(); mAL.add(item); return add(mAL); } APIRequest templ = new APIRequest(ServerCredentials.APIBASE + ServerCredentials.ITEMS + "/" + item.getKey(), "PUT", null); templ.setBody(item); templ.ifMatch = '"' + item.getEtag() + '"'; Log.d(TAG, "etag: " + item.getEtag()); templ.disposition = "xml"; return templ; } /** * Produces API requests to delete queued items from the server. * This request always needs a key. * * From the API docs: * DELETE /users/1/items/ABCD2345 * If-Match: "8e984e9b2a8fb560b0085b40f6c2c2b7" * * @param c * @return */ public static ArrayList<APIRequest> delete(Context c) { ArrayList<APIRequest> list = new ArrayList<APIRequest>(); Database db = new Database(c); String[] args = {}; Cursor cur = db.rawQuery("select item_key, etag from deleteditems", args); if (cur == null) { db.close(); Log.d(TAG, "No deleted items found in database"); return list; } do { APIRequest templ = new APIRequest( ServerCredentials.APIBASE + ServerCredentials.ITEMS + "/" + cur.getString(0), "DELETE", null); templ.disposition = "none"; templ.ifMatch = cur.getString(1); Log.d(TAG, "Adding deleted item: " + cur.getString(0) + " : " + templ.ifMatch); // Save the request to the database to be dispatched later templ.save(db); list.add(templ); } while (cur.moveToNext() != false); cur.close(); db.rawQuery("delete from deleteditems", args); db.close(); return list; } /** * Returns APIRequest objects from the database * @return */ public static ArrayList<APIRequest> queue(Database db) { ArrayList<APIRequest> list = new ArrayList<APIRequest>(); String[] cols = Database.REQUESTCOLS; String[] args = {}; Cursor cur = db.query("apirequests", cols, "", args, null, null, null, null); if (cur == null) return list; do { APIRequest req = new APIRequest(cur); list.add(req); Log.d(TAG, "Queueing request: " + req.query); } while (cur.moveToNext() != false); cur.close(); return list; } }