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; import android.sax.Element; import android.sax.ElementListener; import android.sax.EndTextElementListener; import android.sax.RootElement; import android.sax.StartElementListener; import android.util.Log; import android.util.Xml; import com.crashlytics.android.Crashlytics; 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 com.gimranov.zandy.app.task.APIRequest; import org.json.JSONException; import org.json.JSONObject; import org.xml.sax.Attributes; import org.xml.sax.helpers.DefaultHandler; import java.io.InputStream; import java.util.ArrayList; public class XMLResponseParser extends DefaultHandler { private static final String TAG = "com.gimranov.zandy.app.XMLResponseParser"; private InputStream input; private Item item; private Attachment attachment; private ItemCollection collection; private ItemCollection parent; private String updateType; private String updateKey; private boolean items = false; private APIRequest request; public static boolean followNext = true; public static ArrayList<APIRequest> queue; public static final int MODE_ITEMS = 1; public static final int MODE_ITEM = 2; public static final int MODE_ITEM_CHILDREN = 8; public static final int MODE_COLLECTIONS = 3; public static final int MODE_COLLECTION = 4; public static final int MODE_COLLECTION_ITEMS = 5; public static final int MODE_ENTRY = 6; public static final int MODE_FEED = 7; static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom"; static final String Z_NAMESPACE = "http://zotero.org/ns/api"; public XMLResponseParser(InputStream in, APIRequest request) { followNext = true; input = in; this.request = request; // Initialize the request queue if needed if (queue == null) queue = new ArrayList<APIRequest>(); } public XMLResponseParser(APIRequest request) { followNext = true; this.request = request; // Initialize the request queue if needed if (queue == null) queue = new ArrayList<APIRequest>(); } public void setInputStream(InputStream in) { input = in; } public void update(String type, String key) { updateType = type; updateKey = key; } public void parse(int mode, String url, final Database db) { Element entry; RootElement root; // we have a different root for indiv. items if (mode == MODE_FEED) { root = new RootElement(ATOM_NAMESPACE, "feed"); entry = root.getChild(ATOM_NAMESPACE, "entry"); } else { // MODE_ITEM, MODE_COLLECTION Log.d(TAG, "Parsing in entry mode"); root = new RootElement(ATOM_NAMESPACE, "entry"); entry = (Element) root; } if (mode == MODE_FEED) { root.getChild(ATOM_NAMESPACE, "link").setStartElementListener(new StartElementListener() { public void start(Attributes attributes) { String rel = ""; String href = ""; int length = attributes.getLength(); // I shouldn't have to walk through, but the namespacing isn't working here for (int i = 0; i < length; i++) { if ("rel".equals(attributes.getQName(i))) rel = attributes.getValue(i); if ("href".equals(attributes.getQName(i))) href = attributes.getValue(i); } // We try to get a parent collection if necessary / possible if (rel.contains("self")) { // Try to get a parent collection int colloc = href.indexOf("/collections/"); int itemloc = href.indexOf("/items"); // Our URL looks like this: // https://api.zotero.org/users/5770/collections/2AJUSIU9/items?content=json if (colloc != -1 && itemloc != -1) { // The string "/collections/" is thirteen characters long String id = href.substring(colloc + 13, itemloc); Log.d(TAG, "Collection key: " + id); parent = ItemCollection.load(id, db); if (parent != null) parent.loadChildren(db); } else { Log.d(TAG, "Key extraction failed from root; maybe this isn't a collection listing?"); } } // If there are more items, queue them up to be handled too if (rel.contains("next")) { Log.d(TAG, "Found continuation: " + href); APIRequest req = new APIRequest(href, "get", null); req.query = href; req.disposition = "xml"; queue.add(req); } } }); } entry.setElementListener(new ElementListener() { public void start(Attributes attributes) { item = new Item(); collection = new ItemCollection(); attachment = new Attachment(); Log.d(TAG, "New entry"); } public void end() { if (items == true) { if (updateKey != null && updateType != null && updateType.equals("item")) { // We have an incoming new version of an item Item existing = Item.load(updateKey, db); if (existing != null) { Log.d(TAG, "Updating newly created item to replace temporary key: " + updateKey + " => " + item.getKey() + ""); item.getKey(); existing.dirty = APIRequest.API_CLEAN; // We need to update the parent key in attachments as well, // so they aren't orphaned after we update the item key here ArrayList<Attachment> atts = Attachment.forItem(existing, db); for (Attachment a : atts) { Log.d(TAG, "Propagating item key replacement to attachment with key: " + a.key); a.parentKey = item.getKey(); a.save(db); } // We can't set the new key until after updating child attachments existing.setKey(item.getKey()); if (!existing.getType().equals("attachment")) existing.save(db); } } else if (updateKey != null && updateType != null && updateType.equals("attachment")) { // We have an incoming new version of an item Attachment existing = Attachment.load(updateKey, db); if (existing != null) { Log.d(TAG, "Updating newly created attachment to replace temporary key: " + updateKey + " => " + attachment.key + ""); existing.dirty = APIRequest.API_CLEAN; // we don't change the ZFS status... existing.key = attachment.key; existing.save(db); } } else { item.dirty = APIRequest.API_CLEAN; attachment.dirty = APIRequest.API_CLEAN; if ((attachment.url != null && !"".equals(attachment.url)) || attachment.content.optInt("linkMode") == Attachment.MODE_IMPORTED_FILE || attachment.content.optInt("linkMode") == Attachment.MODE_IMPORTED_URL) attachment.status = Attachment.AVAILABLE; if (!item.getType().equals("attachment") && !item.getType().equals("note")) { Item oldItem = Item.load(item.getKey(), db); // Check timestamps to see if it's different; if not, we should // stop following the Atom continuation links if (oldItem != null && oldItem.getTimestamp().equals(item.getTimestamp())) { followNext = false; } item.save(db); } else { // Don't touch ZFS status here Attachment existing = Attachment.load(attachment.key, db); if (existing != null) { attachment.status = existing.status; } attachment.save(db); } } if (!item.getType().equals("attachment") && !item.getType().equals("note") && item.getChildren() != null && !item.getChildren().equals("0")) { queue.add(APIRequest.children(item)); Log.d(TAG, "Queued children request for item: " + item.getTitle() + " " + item.getKey()); Log.d(TAG, "Item has children: " + item.getChildren()); } // Add to containing collection if (!item.getType().equals("attachment") && parent != null) parent.add(item, true, db); request.getHandler().onUpdate(request); Log.d(TAG, "Done parsing item entry."); return; } if (!items) { if (updateKey != null && updateType != null && updateType.equals("collection")) { // We have an incoming new version of a collection ItemCollection existing = ItemCollection.load(updateKey, db); if (existing != null) { Log.d(TAG, "Updating newly created collection to replace temporary key: " + updateKey + " => " + collection.getKey() + ""); existing.setKey(collection.getKey()); existing.dirty = APIRequest.API_CLEAN; existing.save(db); } Log.d(TAG, "Done parsing new collection entry."); // We don't need to load again, since a new collection can't be stale return; } ItemCollection ic = ItemCollection.load(collection.getKey(), db); if (ic != null) { if (!ic.getTimestamp().equals(collection.getTimestamp())) { // In this case, we have data, but we should refresh it collection.dirty = APIRequest.API_STALE; } else { // Collection hasn't changed! collection = ic; // We also don't need the next page, if we already saw this one followNext = false; } } else { // This means that we haven't seen the collection before, so it must be // a new one, and we don't have contents for it. collection.dirty = APIRequest.API_MISSING; } Log.d(TAG, "Status: " + collection.dirty + " for " + collection.getTitle()); collection.save(db); Log.d(TAG, "Done parsing a collection entry."); return; } } }); entry.getChild(ATOM_NAMESPACE, "title").setEndTextElementListener(new EndTextElementListener() { public void end(String body) { item.setTitle(body); collection.setTitle(body); attachment.title = body; Log.d(TAG, body); } }); entry.getChild(Z_NAMESPACE, "key").setEndTextElementListener(new EndTextElementListener() { public void end(String body) { item.setKey(body); collection.setKey(body); attachment.key = body; Log.d(TAG, body); } }); entry.getChild(ATOM_NAMESPACE, "updated").setEndTextElementListener(new EndTextElementListener() { public void end(String body) { item.setTimestamp(body); collection.setTimestamp(body); Log.d(TAG, body); } }); entry.getChild(Z_NAMESPACE, "itemType").setEndTextElementListener(new EndTextElementListener() { public void end(String body) { item.setType(body); items = true; Log.d(TAG, body); } }); entry.getChild(Z_NAMESPACE, "numChildren").setEndTextElementListener(new EndTextElementListener() { public void end(String body) { item.setChildren(body); Log.d(TAG, body); } }); entry.getChild(Z_NAMESPACE, "year").setEndTextElementListener(new EndTextElementListener() { public void end(String body) { item.setYear(body); Log.d(TAG, body); } }); entry.getChild(Z_NAMESPACE, "creatorSummary").setEndTextElementListener(new EndTextElementListener() { public void end(String body) { item.setCreatorSummary(body); Log.d(TAG, body); } }); entry.getChild(ATOM_NAMESPACE, "id").setEndTextElementListener(new EndTextElementListener() { public void end(String body) { item.setId(body); collection.setId(body); Log.d(TAG, body); } }); entry.getChild(ATOM_NAMESPACE, "link").setStartElementListener(new StartElementListener() { public void start(Attributes attributes) { String rel = ""; String href = ""; int length = attributes.getLength(); // I shouldn't have to walk through, but the namespacing isn't working here for (int i = 0; i < length; i++) { if ("rel".equals(attributes.getQName(i))) rel = attributes.getValue(i); if ("href".equals(attributes.getQName(i))) href = attributes.getValue(i); } if (rel != null && rel.equals("up")) { int start = href.indexOf("/items/"); // Trying to pull out the key of attachment parent attachment.parentKey = href.substring(start + 7, start + 7 + 8); Log.d(TAG, "Setting parentKey to: " + attachment.parentKey); } else if (rel != null && rel.equals("enclosure")) { attachment.url = href; attachment.status = Attachment.AVAILABLE; Log.d(TAG, "url= " + attachment.url); } else if (rel != null) Log.d(TAG, "rel=" + rel + " href=" + href); } }); entry.getChild(ATOM_NAMESPACE, "content").setStartElementListener(new StartElementListener() { public void start(Attributes attributes) { String etag = attributes.getValue(Z_NAMESPACE, "etag"); item.setEtag(etag); collection.setEtag(etag); attachment.etag = etag; Log.d(TAG, "etag: " + etag); } }); entry.getChild(ATOM_NAMESPACE, "content").setEndTextElementListener(new EndTextElementListener() { public void end(String body) { try { JSONObject obj = new JSONObject(body); try { collection.setParent(obj.getString("parent")); } catch (JSONException e) { Log.d(TAG, "No parent found in JSON content; not a subcollection or not a collection"); } item.setContent(obj); attachment.content = obj; } catch (JSONException e) { Log.e(TAG, "JSON parse exception loading content", e); } Log.d(TAG, body); } }); try { Xml.parse(this.input, Xml.Encoding.UTF_8, root.getContentHandler()); if (parent != null) { parent.saveChildren(db); parent.markClean(); parent.save(db); } db.close(); } catch (Exception e) { Log.e(TAG, "exception loading content", e); Crashlytics.logException(new Exception("Exception parsing data", e)); } } }