/** * Java Zeitgeist API * Copyright (C) 2012 Matthias Hecker <http://apoc.cc/> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package li.zeitgeist.api; import li.zeitgeist.api.error.*; import java.io.*; import java.net.URLEncoder; import java.util.*; import com.google.gson.Gson; import com.google.gson.JsonParseException; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.entity.AbstractHttpEntity; import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.entity.mime.content.FileBody; import org.apache.http.entity.mime.content.StringBody; import org.apache.http.impl.client.AbstractHttpClient; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; /** * Zeitgeist API methods. * * Uses a HTTP interface to communicate with a zeitgeist * installation. */ public class ZeitgeistApi { /** * Apache HTTP Client instance. */ private AbstractHttpClient client; /** * Base URL of the zeitgeist installation. */ private String baseUrl; /** * User eMail for authentication. */ private String email = null; /** * User API Secret used for authentication. */ private String apiSecret = null; /** * User Id we authenticated. */ private int userId = -1; /** * Construct a API instance with the provided baseUrl. * @param baseUrl */ public ZeitgeistApi(String baseUrl) { this.baseUrl = baseUrl; this.client = new DefaultHttpClient(); } /** * Construct a API instance with provided base URL and authentication. * @param baseUrl * @param email * @param apiSecret * @throws ZeitgeistError */ public ZeitgeistApi(String baseUrl, String email, String apiSecret) { this.baseUrl = baseUrl; this.email = email; this.apiSecret = apiSecret; this.client = new DefaultHttpClient(); if (!apiSecret.equals("")) { try { getApiSecret(); // tests the secret (caches the userId) } catch (ZeitgeistError e) { e.printStackTrace(); email = apiSecret = ""; } } } /** * Upload image as a new item. * @param file * @return the created item instance * @throws ZeitgeistError */ public Item createByFile(File file) throws ZeitgeistError { return createByFile(file, ""); } /** * Upload image as a new item, assign with tags provided. * @param file File instance * @param tags Comma seperated list of tags. * @return the created item instance * @throws ZeitgeistError */ public Item createByFile(File file, String tags) throws ZeitgeistError { return createByFile(file, tags, false); } /** * Upload image as a new item, assign with tags provided and announce. * @param file File instance * @param tags Array of tag strings. * @param announce True if the item should be announced in irc. * @return the created item instance * @throws ZeitgeistError */ public Item createByFile(File file, List<String> tags, boolean announce) throws ZeitgeistError { return createByFile(file, Utils.join(tags.toArray(new String[0]), ","), announce); } /** * Upload image as a new item, assign with tags provided and announce. * @param file File instance * @param tags Comma seperated list of tags. * @param announce True if the item should be announced in irc. * @return the created item instance * @throws ZeitgeistError */ public Item createByFile(File file, String tags, boolean announce) throws ZeitgeistError { List<File> files = new Vector<File>(); files.add(file); return createByFiles(files, tags, announce).get(0); } /** * Upload multiple files at once. * @param files * @return list of created items * @throws ZeitgeistError */ public List<Item> createByFiles(List<File> files) throws ZeitgeistError { return createByFiles(files, ""); } /** * Upload multiple files at once with tags. * @param files * @param tags Comma seperated list of tags. * @return list of created items * @throws ZeitgeistError */ public List<Item> createByFiles(List<File> files, String tags) throws ZeitgeistError { return createByFiles(files, tags, false); } /** * Upload multiple files at once with tags and announce. * @param files * @param tags Array of tag strings. * @param announce True if the item should be announced in irc. * @return list of created items * @throws ZeitgeistError */ public List<Item> createByFiles(List<File> files, List<String> tags, boolean announce) throws ZeitgeistError { return createByFiles(files, Utils.join(tags.toArray(new String[0]), ","), announce); } /** * Upload multiple files at once with tags and announce. * @param files * @param tags Comma seperated list of tags. * @param announce True if the item should be announced in irc. * @return list of created items * @throws ZeitgeistError */ public List<Item> createByFiles(List<File> files, String tags, boolean announce) throws ZeitgeistError { return createByFiles(files, tags, announce, null); } public List<Item> createByFiles(List<File> files, String tags, boolean announce, OnProgressListener listener) throws ZeitgeistError { MultipartEntity entity; if (listener == null) { entity = new MultipartEntity(); } else { entity = new MultipartEntityWithProgress(listener); } for (File file : files) { entity.addPart("image_upload[]", new FileBody(file)); } try { entity.addPart("tags", new StringBody(tags)); entity.addPart("announce", new StringBody(announce ? "true" : "false")); } catch (UnsupportedEncodingException e) { throw new ZeitgeistError("UnsupportedEncoding: " + e.getMessage()); } Map<String, ?> jsonObject = postRequest("/new", entity); ArrayList<Map<String, ?>> itemObjects = (ArrayList<Map<String, ?>>)jsonObject.get("items"); List<Item> items = new Vector<Item>(); for (Map<String, ?> itemObject : itemObjects) { items.add(new Item(itemObject, baseUrl)); } return items; } public interface OnProgressListener { public void onProgress(long transferred); } // based on this idea: // http://toolongdidntread.com/android/android-multipart-post-with-progress-bar/ private class MultipartEntityWithProgress extends MultipartEntity { private OnProgressListener listener; public MultipartEntityWithProgress(OnProgressListener listener) { this.listener = listener; } @Override public void writeTo(final OutputStream out) throws IOException { super.writeTo(new CountingOutputStream(out, this.listener)); } private class CountingOutputStream extends FilterOutputStream { private final OnProgressListener listener; private long transferred; public CountingOutputStream(final OutputStream out, final OnProgressListener listener) { super(out); this.listener = listener; this.transferred = 0; } public void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); this.transferred += len; this.listener.onProgress(this.transferred); } public void write(int b) throws IOException { out.write(b); this.transferred++; this.listener.onProgress(this.transferred); } } } /** * Remote upload/create by image/video/audio url. * @param url * @return the item instance created. * @throws ZeitgeistError */ public Item createByUrl(String url) throws ZeitgeistError { return createByUrl(url, ""); } /** * Remote upload/create by image/video/audio url with tags. * @param url * @param tags Comma seperated list of tags. * @return the item instance created. * @throws ZeitgeistError */ public Item createByUrl(String url, String tags) throws ZeitgeistError { return createByUrl(url, tags, false); } /** * Remote upload/create by image/video/audio url with tags and announce. * @param url * @param tags Array of tag strings. * @return the item instance created. * @throws ZeitgeistError */ public Item createByUrl(String url, List<String> tags, boolean announce) throws ZeitgeistError { return createByUrl(url, Utils.join(tags.toArray(new String[0]), ","), announce); } /** * Remote upload/create by image/video/audio url with tags and announce. * @param url * @param tags Comma seperated list of tags. * @return the item instance created. * @throws ZeitgeistError */ public Item createByUrl(String url, String tags, boolean announce) throws ZeitgeistError { List<String> urls = new Vector<String>(); urls.add(url); return createByUrls(urls, tags, announce).get(0); } /** * Multiple remote upload/create by image/video/audio url. * @param urls list of URLs * @return array of item instances created. * @throws ZeitgeistError */ public List<Item> createByUrls(List<String> urls) throws ZeitgeistError { return createByUrls(urls, ""); } /** * Multiple remote upload/create by image/video/audio url with tags. * @param urls list of URLs * @param tags Comma seperated list of tags. * @return array of item instances created. * @throws ZeitgeistError */ public List<Item> createByUrls(List<String> urls, String tags) throws ZeitgeistError { return createByUrls(urls, tags, false); } /** * Multiple remote upload/create by image/video/audio url with tags and announce. * @param urls list of URLs * @param tags Array of tag strings. * @return array of item instances created. * @throws ZeitgeistError */ public List<Item> createByUrls(List<String> urls, List<String> tags, boolean announce) throws ZeitgeistError { return createByUrls(urls, Utils.join(tags.toArray(new String[0]), ","), announce); } /** * Multiple remote upload/create by image/video/audio url with tags and announce. * @param urls list of URLs * @param tags Comma seperated list of tags. * @return array of item instances created. * @throws ZeitgeistError */ public List<Item> createByUrls(List<String> urls, String tags, boolean announce) throws ZeitgeistError { List<NameValuePair> postData = new ArrayList<NameValuePair>(); for (String url : urls) { postData.add(new BasicNameValuePair("remote_url[]", url)); } postData.add(new BasicNameValuePair("tags", tags)); postData.add(new BasicNameValuePair("announce", announce ? "true" : "false")); Map<String, ?> jsonObject = postRequest("/new", createEntityByNameValueList(postData)); ArrayList<Map<String, ?>> itemObjects = (ArrayList<Map<String, ?>>)jsonObject.get("items"); List<Item> items = new Vector<Item>(); for (Map<String, ?> itemObject : itemObjects) { items.add(new Item(itemObject, baseUrl)); } return items; } /** * Query for a single item instance by ID. * @param id * @return the item instance * @throws ZeitgeistError */ public Item item(int id) throws ZeitgeistError { Map<String, ?> jsonObject = getRequest("/" + String.valueOf(id)); Item item = new Item((Map<String, ?>)jsonObject.get("item"), baseUrl); return item; } /** * Lists the newest/frontpage items. * @return list of item objects. * @throws ZeitgeistError */ public List<Item> list() throws ZeitgeistError { return list(-1, -1); } /** * Lists items that come before a specified ID. * @param before ID * @return list of item objects. * @throws ZeitgeistError */ public List<Item> listBefore(int before) throws ZeitgeistError { return list(before, -1); } /** * Lists items that come after a specified ID. * @param after ID * @return list of item objects. * @throws ZeitgeistError */ public List<Item> listAfter(int after) throws ZeitgeistError { return list(-1, after); } /** * Lists items that come before or after specified IDs. * @param before ID (optional -1) * @param after ID (optional -1) * @return list of item objects. * @throws ZeitgeistError */ public List<Item> list(int before, int after) throws ZeitgeistError { StringBuilder query = new StringBuilder().append("/"); if (before >= 0 || after >= 0) { query.append("?"); if (before >= 0) query.append("before=" + String.valueOf(before)); if (after >= 0) query.append("after=" + String.valueOf(after)); } Map<String, ?> jsonObject = getRequest(query.toString()); ArrayList<Map<String, ?>> itemObjects = (ArrayList<Map<String, ?>>)jsonObject.get("items"); List<Item> items = new Vector<Item>(); for (Map<String, ?> itemObject : itemObjects) { items.add(new Item(itemObject, baseUrl)); } return items; } /** * Search for tags by partial name. * @param query * @return list of tag objects. * @throws ZeitgeistError */ public List<Tag> searchTags(String query) throws ZeitgeistError { List<NameValuePair> postData = new ArrayList<NameValuePair>(); postData.add(new BasicNameValuePair("q", query)); Map<String, ?> jsonObject = postRequest("/search", createEntityByNameValueList(postData)); ArrayList<Map<String, ?>> tagObjects = (ArrayList<Map<String, ?>>)jsonObject.get("tags"); List<Tag> tags = new Vector<Tag>(); for (Map<String, ?> tagObject : tagObjects) { tags.add(new Tag(tagObject)); } return tags; } /** * List newest items that are associated with given tag. * @param tag * @return list of item objects. * @throws ZeitgeistError */ public List<Item> listByTag(String tag) throws ZeitgeistError { return listByTag(tag, -1, -1); } /** * List items that are associated with given tag and come before specified ID. * @param tag * @param before ID (optional -1) * @return list of item objects. * @throws ZeitgeistError */ public List<Item> listByTagBefore(String tag, int before) throws ZeitgeistError { return listByTag(tag, before, -1); } /** * List items that are associated with given tag and come after specified ID. * @param tag * @param after ID (optional -1) * @return list of item objects. * @throws ZeitgeistError */ public List<Item> listByTagAfter(String tag, int after) throws ZeitgeistError { return listByTag(tag, -1, after); } /** * List items that are associated with given tag and come after or before specified IDs. * @param tag * @param before ID (optional -1) * @param after ID (optional -1) * @return list of item objects. * @throws ZeitgeistError */ public List<Item> listByTag(String tag, int before, int after) throws ZeitgeistError { StringBuilder query = new StringBuilder(); try { query.append("/show/tag/" + URLEncoder.encode(tag, "utf-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } if (before >= 0 || after >= 0) { query.append("?"); if (before >= 0) query.append("before=" + String.valueOf(before)); if (after >= 0) query.append("after=" + String.valueOf(after)); } Map<String, ?> jsonObject = getRequest(query.toString()); ArrayList<Map<String, ?>> itemObjects = (ArrayList<Map<String, ?>>)jsonObject.get("items"); List<Item> items = new Vector<Item>(); for (Map<String, ?> itemObject : itemObjects) { items.add(new Item(itemObject, baseUrl)); } return items; } /** * Update the tags of a item. * * This adds or removes taggings from an item that is specified * by ID. Tags is a comma seperated list with tags, each tag * can be prefixed by + or - to specify to add or delete a tag, * note that + is optional due to be the default. * * @param id * @param tags * @return the updated item. * @throws ZeitgeistError */ public Item update(int id, String tags) throws ZeitgeistError { Vector<String> addTags = new Vector<String>(); Vector<String> delTags = new Vector<String>(); String[] tagsArray = tags.split(","); for (String tag : tagsArray) { tag = tag.trim(); if (tag.charAt(0) == '-') { tag = tag.substring(1); delTags.add(tag); } else { if (tag.charAt(0) == '+') { tag = tag.substring(1); } addTags.add(tag); } } return this.update(id, addTags.toArray(new String[0]), delTags.toArray(new String[0])); } /** * Update the tags of a item. * * This adds or removes taggings from an item that is specified * by ID. * * @param id * @param addTags array of tags to add * @param delTags array of tags to delete * @return the updated item. * @throws ZeitgeistError */ public Item update(int id, String[] addTags, String[] delTags) throws ZeitgeistError { List<NameValuePair> postData = new ArrayList<NameValuePair>(); postData.add(new BasicNameValuePair("id", String.valueOf(id))); postData.add(new BasicNameValuePair("add_tags", Utils.join(addTags, ","))); postData.add(new BasicNameValuePair("del_tags", Utils.join(delTags, ","))); Map<String, ?> jsonObject = postRequest("/update", createEntityByNameValueList(postData)); Item item = new Item((Map<String, ?>)jsonObject.get("item"), baseUrl); return item; } /** * Delete a item specified by ID. * * Only the creator (owner) or an admin can delete items. * * @param id * @return the ID of the deleted item. * @throws ZeitgeistError */ public int delete(int id) throws ZeitgeistError { List<NameValuePair> postData = new ArrayList<NameValuePair>(); postData.add(new BasicNameValuePair("id", String.valueOf(id))); Map<String, ?> jsonObject = postRequest("/delete", createEntityByNameValueList(postData)); return ((Double)jsonObject.get("id")).intValue(); } /** * Upvote (+1) an item specified by ID. * @param id * @return number of upvotes the item has. * @throws ZeitgeistError */ public int upvote(int id) throws ZeitgeistError { return upvote(id, false); } /** * Upvote (+1) an item specified by ID. * * The remove parameter specify if the upvote should be undone/removed. * * @param id * @param remove True if the upvote should be deleted. * @return number of upvotes the item has. * @throws ZeitgeistError */ public int upvote(int id, boolean remove) throws ZeitgeistError { List<NameValuePair> postData = new ArrayList<NameValuePair>(); postData.add(new BasicNameValuePair("id", String.valueOf(id))); postData.add(new BasicNameValuePair("remove", remove ? "true" : "false")); Map<String, ?> jsonObject = postRequest("/upvote", createEntityByNameValueList(postData)); return ((Double)jsonObject.get("upvotes")).intValue(); } /** * Request the api secret, useful for testing. * * This also caches the userId. * * @return String the api secret key. * @throws ZeitgeistError */ public String getApiSecret() throws ZeitgeistError { Map<String, ?> jsonObject = getRequest("/api_secret"); if (jsonObject.containsKey("user_id")) { userId = ((Double)jsonObject.get("user_id")).intValue(); } return (String) jsonObject.get("api_secret"); } /** * Test the supplied url, email and key, return true if ok. * * @param baseUrl * @param eMail * @param apiSecret * @return true if successfully authenticated. */ public boolean testAuth(String baseUrl, String eMail, String apiSecret) { ZeitgeistApi api = new ZeitgeistApi(baseUrl, eMail, apiSecret); try { api.getApiSecret(); return true; } catch (ZeitgeistError error) { return false; } } /** * Creates urlencoded data from a pair list for POST requests. * @param postData * @return entity * @throws ZeitgeistError */ private HttpEntity createEntityByNameValueList(List<NameValuePair> postData) throws ZeitgeistError { try { return new UrlEncodedFormEntity(postData, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new ZeitgeistError("UnsupportedEncoding: " + e.getMessage()); } } /** * Perform a POST request. * @param query URI from url base. * @param entity * @return json map of primitives. * @throws ZeitgeistError */ private Map<String, ?> postRequest(String query, HttpEntity entity) throws ZeitgeistError { Map<String, ?> jsonObject = null; HttpPost request = new HttpPost(this.baseUrl + query); setHeaders(request); request.setEntity(entity); jsonObject = executeRequest(request); return jsonObject; } /** * Perform a GET request. * @param query URI from url base. * @return json map of primitives. * @throws ZeitgeistError */ private Map<String, ?> getRequest(String query) throws ZeitgeistError { Map<String, ?> jsonObject = null; HttpGet request = new HttpGet(this.baseUrl + query); setHeaders(request); jsonObject = executeRequest(request); return jsonObject; } /** * Execute a HTTP request and parse the result as JSON, also unifies * Exceptions into the ZeitgeistError class. * @param request * @return json map of primitives. * @throws ZeitgeistError */ private Map<String, ?> executeRequest(HttpRequestBase request) throws ZeitgeistError { Map<String, ?> jsonObject = null; try { HttpResponse response = this.client.execute(request); String jsonString = EntityUtils.toString(response.getEntity()); jsonObject = parseJson(jsonString); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200) { // parse json into an ZeitgeistException and throw ZeitgeistError error = null; if (((String)jsonObject.get("type")).equals("CreateItemError")) { error = new CreateItemError(jsonObject, baseUrl); } else { error = new ZeitgeistError(jsonObject); } throw error; } } catch (ClientProtocolException e) { throw new ZeitgeistError("ClientProtocolException: " + e.getMessage()); } catch (IOException e) { throw new ZeitgeistError("IOException: " + e.getMessage()); } catch (ZeitgeistError e) { throw e; // just passthrough } return jsonObject; } /** * Set required Headers for the API, json accept only and authentication. * @param request */ private void setHeaders(HttpRequestBase request) { request.setHeader("Accept", "application/json"); if (this.email != null && this.apiSecret != null && this.email.length() > 0 && this.apiSecret.length() > 0) { request.setHeader("X-API-Auth", this.email + "|" + this.apiSecret); } } /** * Parses json by string, returns a map of primitives by string key. * @param jsonString * @return json "primitive" map * @throws ZeitgeistError */ private Map<String, ?> parseJson(String jsonString) throws ZeitgeistError { Map<String, ?> json = null; try { json = new Gson().fromJson(jsonString, Map.class); } catch (JsonParseException e) { throw new ZeitgeistError("JsonParseException: " + e.getMessage()); } return json; } /** * The Base URL used by this API instance. * @return string URL */ public String getBaseUrl() { return baseUrl; } /** * Return the User Id, returned by the /api_secret. * * @return integer, -1 if error */ public int getUserId() { return userId; } }