/**
* 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;
}
}