/*
* PinDroid - http://code.google.com/p/PinDroid/
*
* Copyright (C) 2010 Matt Schmidt
*
* PinDroid 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.
*
* PinDroid 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 PinDroid; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA
*/
package com.pindroid.client;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.TreeMap;
import java.util.zip.GZIPInputStream;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import com.pindroid.Constants;
import com.pindroid.providers.BookmarkContent.Bookmark;
import com.pindroid.providers.NoteContent.Note;
import com.pindroid.providers.TagContent.Tag;
import com.pindroid.xml.SaxBookmarkParser;
import com.pindroid.xml.SaxNoteListParser;
import com.pindroid.xml.SaxNoteParser;
import com.pindroid.xml.SaxResultParser;
import com.pindroid.xml.SaxTagParser;
import com.pindroid.xml.SaxTokenParser;
import com.pindroid.xml.SaxUpdateParser;
public class PinboardApi {
private static final String TAG = "PinboardApi";
public static final String AUTH_TOKEN_URI = "v1/user/api_token";
public static final String FETCH_TAGS_URI = "v1/tags/get";
public static final String FETCH_SUGGESTED_TAGS_URI = "v1/posts/suggest";
public static final String FETCH_BOOKMARKS_URI = "v1/posts/all";
public static final String FETCH_CHANGED_BOOKMARKS_URI = "v1/posts/all";
public static final String FETCH_BOOKMARK_URI = "v1/posts/get";
public static final String LAST_UPDATE_URI = "v1/posts/update";
public static final String DELETE_BOOKMARK_URI = "v1/posts/delete";
public static final String ADD_BOOKMARKS_URI = "v1/posts/add";
public static final String FETCH_SECRET_URI = "v1/user/secret";
public static final String FETCH_NOTE_LIST_URI = "v1/notes/list";
public static final String FETCH_NOTE_DETAILS_URI = "v1/notes/";
private static final String SCHEME = "https";
private static final String PINBOARD_AUTHORITY = "api.pinboard.in";
private static final int PORT = 443;
private static final AuthScope SCOPE = new AuthScope(PINBOARD_AUTHORITY, PORT);
/**
* Attempts to authenticate to Pinboard using a legacy Pinboard account.
*
* @param username The user's username.
* @param password The user's password.
* @param handler The hander instance from the calling UI thread.
* @param context The context of the calling Activity.
* @return The boolean result indicating whether the user was
* successfully authenticated.
* @throws
*/
public static String pinboardAuthenticate(String username, String password) {
final HttpResponse resp;
Uri.Builder builder = new Uri.Builder();
builder.scheme(SCHEME);
builder.authority(PINBOARD_AUTHORITY);
builder.appendEncodedPath(AUTH_TOKEN_URI);
Uri uri = builder.build();
HttpGet request = new HttpGet(String.valueOf(uri));
DefaultHttpClient client = (DefaultHttpClient)HttpClientFactory.getThreadSafeClient();
CredentialsProvider provider = client.getCredentialsProvider();
Credentials credentials = new UsernamePasswordCredentials(username, password);
provider.setCredentials(SCOPE, credentials);
try {
resp = client.execute(request);
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
final HttpEntity entity = resp.getEntity();
InputStream instream = entity.getContent();
SaxTokenParser parser = new SaxTokenParser(instream);
PinboardAuthToken token = parser.parse();
instream.close();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Successful authentication");
Log.v(TAG, "AuthToken: " + token.getToken());
}
return token.getToken();
} else {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Error authenticating" + resp.getStatusLine());
}
return null;
}
} catch (final IOException e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "IOException when getting authtoken", e);
}
return null;
} catch (ParseException e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "ParseException when getting authtoken", e);
}
return null;
} finally {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "getAuthtoken completing");
}
}
}
/**
* Gets timestamp of last update to data on Pinboard servers.
*
* @param account The account being synced.
* @param context The current application context.
* @return An Update object containing the timestamp and the number of new bookmarks in the
* inbox.
* @throws IOException If a server error was encountered.
* @throws AuthenticationException If an authentication error was encountered.
* @throws TooManyRequestsException
* @throws ParseException
* @throws PinboardException
*/
public static Update lastUpdate(Account account, Context context)
throws IOException, AuthenticationException, TooManyRequestsException, ParseException, PinboardException {
InputStream responseStream = null;
TreeMap<String, String> params = new TreeMap<String, String>();
responseStream = PinboardApiCall(LAST_UPDATE_URI, params, account, context);
SaxUpdateParser parser = new SaxUpdateParser(responseStream);
Update update = parser.parse();
responseStream.close();
return update;
}
/**
* Sends a request to Pinboard's Add Bookmark api.
*
* @param bookmark The bookmark to be added.
* @param account The account being synced.
* @param context The current application context.
* @return A boolean indicating whether or not the api call was successful.
* @throws IOException If an IO error was encountered.
* @throws TooManyRequestsException
* @throws AuthenticationException If an authentication error was encountered.
* @throws PinboardException If a server error is encountered.
* @throws ParseException
* @throws Exception If an unknown error is encountered.
*/
public static Boolean addBookmark(Bookmark bookmark, Account account, Context context)
throws IOException, AuthenticationException, TooManyRequestsException, PinboardException, ParseException {
String url = bookmark.getUrl();
if(url.endsWith("/")) {
url = url.substring(0, url.lastIndexOf('/'));
}
TreeMap<String, String> params = new TreeMap<String, String>();
params.put("description", bookmark.getDescription());
params.put("extended", bookmark.getNotes());
params.put("tags", bookmark.getTagString());
params.put("url", bookmark.getUrl());
if(bookmark.getShared()){
params.put("shared", "yes");
} else params.put("shared", "no");
if(bookmark.getToRead()){
params.put("toread", "yes");
}
String uri = ADD_BOOKMARKS_URI;
InputStream responseStream = null;
responseStream = PinboardApiCall(uri, params, account, context);
SaxResultParser parser = new SaxResultParser(responseStream);
PinboardApiResult result = parser.parse();
responseStream.close();
if (result.getCode().equalsIgnoreCase("done")) {
return true;
} else if (result.getCode().equalsIgnoreCase("something went wrong")) {
Log.e(TAG, "Pinboard server error in adding bookmark");
throw new PinboardException();
} else {
Log.e(TAG, "IO error in adding bookmark");
throw new IOException();
}
}
/**
* Sends a request to Pinboard's Delete Bookmark api.
*
* @param bookmark The bookmark to be deleted.
* @param account The account being synced.
* @param context The current application context.
* @return A boolean indicating whether or not the api call was successful.
* @throws IOException If a server error was encountered.
* @throws AuthenticationException If an authentication error was encountered.
* @throws TooManyRequestsException
* @throws ParseException
* @throws PinboardException
*/
public static Boolean deleteBookmark(Bookmark bookmark, Account account, Context context)
throws IOException, AuthenticationException, TooManyRequestsException, ParseException, PinboardException {
TreeMap<String, String> params = new TreeMap<String, String>();
InputStream responseStream = null;
String url = DELETE_BOOKMARK_URI;
params.put("url", bookmark.getUrl());
responseStream = PinboardApiCall(url, params, account, context);
SaxResultParser parser = new SaxResultParser(responseStream);
PinboardApiResult result = parser.parse();
responseStream.close();
if (result.getCode().equalsIgnoreCase("done") || result.getCode().equalsIgnoreCase("item not found")) {
return true;
} else {
Log.e(TAG, "Server error in fetching bookmark list");
throw new IOException();
}
}
/**
* Retrieves a specific list of bookmarks from Pinboard.
*
* @param hashes A list of bookmark hashes to be retrieved.
* The hashes are MD5 hashes of the URL of the bookmark.
*
* @param account The account being synced.
* @param context The current application context.
* @return A list of bookmarks received from the server.
* @throws IOException If a server error was encountered.
* @throws AuthenticationException If an authentication error was encountered.
* @throws TooManyRequestsException
* @throws PinboardException
*/
public static ArrayList<Bookmark> getBookmark(ArrayList<String> hashes, Account account,
Context context) throws IOException, AuthenticationException, TooManyRequestsException, PinboardException {
ArrayList<Bookmark> bookmarkList = new ArrayList<Bookmark>();
TreeMap<String, String> params = new TreeMap<String, String>();
String hashString = "";
InputStream responseStream = null;
String url = FETCH_BOOKMARK_URI;
for(String h : hashes){
if(hashes.get(0) != h){
hashString += "+";
}
hashString += h;
}
params.put("meta", "yes");
params.put("hashes", hashString);
responseStream = PinboardApiCall(url, params, account, context);
SaxBookmarkParser parser = new SaxBookmarkParser(responseStream);
try {
bookmarkList = parser.parse();
} catch (ParseException e) {
Log.e(TAG, "Server error in fetching bookmark list");
throw new IOException();
}
responseStream.close();
return bookmarkList;
}
/**
* Retrieves the entire list of bookmarks for a user from Pinboard.
*
* @param tagname If specified, will only retrieve bookmarks with a specific tag.
* @param account The account being synced.
* @param context The current application context.
* @return A list of bookmarks received from the server.
* @throws IOException If a server error was encountered.
* @throws AuthenticationException If an authentication error was encountered.
* @throws TooManyRequestsException
* @throws PinboardException
*/
public static ArrayList<Bookmark> getAllBookmarks(String tagName, Account account, Context context)
throws IOException, AuthenticationException, TooManyRequestsException, PinboardException {
return getAllBookmarks(tagName, 0, 0, account, context);
}
/**
* Retrieves the entire list of bookmarks for a user from Pinboard.
*
* @param tagname If specified, will only retrieve bookmarks with a specific tag.
* @param start Bookmark number to start from.
* @param count Number of results to retrieve.
* @param account The account being synced.
* @param context The current application context.
* @return A list of bookmarks received from the server.
* @throws IOException If a server error was encountered.
* @throws AuthenticationException If an authentication error was encountered.
* @throws TooManyRequestsException
* @throws PinboardException
*/
public static ArrayList<Bookmark> getAllBookmarks(String tagName, int start, int count, Account account, Context context)
throws IOException, AuthenticationException, TooManyRequestsException, PinboardException {
ArrayList<Bookmark> bookmarkList = new ArrayList<Bookmark>();
InputStream responseStream = null;
TreeMap<String, String> params = new TreeMap<String, String>();
String url = FETCH_BOOKMARKS_URI;
if(tagName != null && tagName != ""){
params.put("tag", tagName);
}
if(start != 0){
params.put("start", Integer.toString(start));
}
if(count != 0){
params.put("results", Integer.toString(count));
}
params.put("meta", "yes");
responseStream = PinboardApiCall(url, params, account, context);
SaxBookmarkParser parser = new SaxBookmarkParser(responseStream);
try {
bookmarkList = parser.parse();
} catch (ParseException e) {
Log.e(TAG, "Server error in fetching bookmark list");
throw new IOException();
}
responseStream.close();
return bookmarkList;
}
/**
* Retrieves a list of suggested tags for a URL.
*
* @param suggestUrl The URL to get suggested tags for.
* @param account The account being synced.
* @param context The current application context.
* @return A list of tags suggested for the provided url.
* @throws IOException If a server error was encountered.
* @throws AuthenticationException If an authentication error was encountered.
* @throws TooManyRequestsException
* @throws PinboardException
*/
public static ArrayList<Tag> getSuggestedTags(String suggestUrl, Account account, Context context)
throws IOException, AuthenticationException, TooManyRequestsException, PinboardException {
ArrayList<Tag> tagList = new ArrayList<Tag>();
if(!suggestUrl.startsWith("http")){
suggestUrl = "http://" + suggestUrl;
}
InputStream responseStream = null;
TreeMap<String, String> params = new TreeMap<String, String>();
params.put("url", suggestUrl);
String url = FETCH_SUGGESTED_TAGS_URI;
responseStream = PinboardApiCall(url, params, account, context);
SaxTagParser parser = new SaxTagParser(responseStream);
try {
tagList = parser.parseSuggested();
} catch (ParseException e) {
Log.e(TAG, "Server error in fetching bookmark list");
throw new IOException();
}
responseStream.close();
return tagList;
}
/**
* Retrieves a list of all tags for a user from Pinboard.
*
* @param account The account being synced.
* @param context The current application context.
* @return A list of the users tags.
* @throws IOException If a server error was encountered.
* @throws AuthenticationException If an authentication error was encountered.
* @throws TooManyRequestsException
* @throws PinboardException
*/
public static ArrayList<Tag> getTags(Account account, Context context)
throws IOException, AuthenticationException, TooManyRequestsException, PinboardException {
ArrayList<Tag> tagList = new ArrayList<Tag>();
InputStream responseStream = null;
final TreeMap<String, String> params = new TreeMap<String, String>();
responseStream = PinboardApiCall(FETCH_TAGS_URI, params, account, context);
final SaxTagParser parser = new SaxTagParser(responseStream);
try {
tagList = parser.parse();
} catch (ParseException e) {
Log.e(TAG, "Server error in fetching bookmark list");
throw new IOException();
}
responseStream.close();
return tagList;
}
/**
* Gets the users secret rss token.
*
* @param account The account being synced.
* @param context The current application context.
* @return The secret rss token.
* @throws IOException If a server error was encountered.
* @throws AuthenticationException If an authentication error was encountered.
* @throws TooManyRequestsException
* @throws ParseException
* @throws PinboardException
*/
public static String getSecretToken(Account account, Context context)
throws IOException, AuthenticationException, TooManyRequestsException, ParseException, PinboardException {
InputStream responseStream = null;
final TreeMap<String, String> params = new TreeMap<String, String>();
responseStream = PinboardApiCall(FETCH_SECRET_URI, params, account, context);
SaxTokenParser parser = new SaxTokenParser(responseStream);
PinboardAuthToken token = parser.parse();
responseStream.close();
return token.getToken();
}
/**
* Retrieves a list of all notes for a user from Pinboard.
*
* @param account The account being synced.
* @param context The current application context.
* @return A list of the users notes.
* @throws IOException If a server error was encountered.
* @throws AuthenticationException If an authentication error was encountered.
* @throws TooManyRequestsException
* @throws PinboardException
*/
public static ArrayList<Note> getNoteList(Account account, Context context)
throws IOException, AuthenticationException, TooManyRequestsException, PinboardException {
ArrayList<Note> noteList = new ArrayList<Note>();
InputStream responseStream = null;
final TreeMap<String, String> params = new TreeMap<String, String>();
responseStream = PinboardApiCall(FETCH_NOTE_LIST_URI, params, account, context);
final SaxNoteListParser parser = new SaxNoteListParser(responseStream);
try {
noteList = parser.parse();
} catch (ParseException e) {
Log.e(TAG, "Server error in fetching bookmark list");
throw new IOException();
}
responseStream.close();
return noteList;
}
/**
* Retrieves details for a note for a user from Pinboard.
*
* @param account The account being synced.
* @param context The current application context.
* @return A note.
* @throws IOException If a server error was encountered.
* @throws AuthenticationException If an authentication error was encountered.
* @throws TooManyRequestsException
* @throws PinboardException
*/
public static Note getNote(String pid, Account account, Context context)
throws IOException, AuthenticationException, TooManyRequestsException, PinboardException {
Note note = new Note();
InputStream responseStream = null;
final TreeMap<String, String> params = new TreeMap<String, String>();
responseStream = PinboardApiCall(FETCH_NOTE_DETAILS_URI + pid, params, account, context);
final SaxNoteParser parser = new SaxNoteParser(responseStream);
try {
note = parser.parse();
} catch (ParseException e) {
Log.e(TAG, "Server error in fetching bookmark list");
throw new IOException();
}
responseStream.close();
return note;
}
/**
* Performs an api call to Pinboard's http based api methods.
*
* @param url URL of the api method to call.
* @param params Extra parameters included in the api call, as specified by different methods.
* @param account The account being synced.
* @param context The current application context.
* @return A String containing the response from the server.
* @throws IOException If a server error was encountered.
* @throws AuthenticationException If an authentication error was encountered.
* @throws TooManyRequestsException
* @throws PinboardException
*/
private static InputStream PinboardApiCall(String url, TreeMap<String, String> params,
Account account, Context context) throws IOException, AuthenticationException, TooManyRequestsException, PinboardException{
final AccountManager am = AccountManager.get(context);
if(account == null)
throw new AuthenticationException();
final String username = account.name;
String authtoken = "00000000000000000000"; // need to provide a sane default value, since a token that is too short causes a 500 error instead of 401
try {
String tempAuthtoken = am.blockingGetAuthToken(account, Constants.AUTHTOKEN_TYPE, true);
if(tempAuthtoken != null)
authtoken = tempAuthtoken;
} catch (Exception e) {
e.printStackTrace();
throw new AuthenticationException("Error getting auth token");
}
params.put("auth_token", username + ":" + authtoken);
final Uri.Builder builder = new Uri.Builder();
builder.scheme(SCHEME);
builder.authority(PINBOARD_AUTHORITY);
builder.appendEncodedPath(url);
for(String key : params.keySet()){
builder.appendQueryParameter(key, params.get(key));
}
String apiCallUrl = builder.build().toString();
Log.d("apiCallUrl", apiCallUrl);
final HttpGet post = new HttpGet(apiCallUrl);
post.setHeader("User-Agent", "PinDroid");
post.setHeader("Accept-Encoding", "gzip");
final DefaultHttpClient client = (DefaultHttpClient)HttpClientFactory.getThreadSafeClient();
final HttpResponse resp = client.execute(post);
final int statusCode = resp.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
final HttpEntity entity = resp.getEntity();
InputStream instream = entity.getContent();
final Header encoding = entity.getContentEncoding();
if(encoding != null && encoding.getValue().equalsIgnoreCase("gzip")) {
instream = new GZIPInputStream(instream);
}
return instream;
} else if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
am.invalidateAuthToken(Constants.AUTHTOKEN_TYPE, authtoken);
try {
authtoken = am.blockingGetAuthToken(account, Constants.AUTHTOKEN_TYPE, true);
} catch (Exception e) {
e.printStackTrace();
throw new AuthenticationException("Invalid auth token");
}
throw new AuthenticationException();
} else if (statusCode == Constants.HTTP_STATUS_TOO_MANY_REQUESTS) {
throw new TooManyRequestsException(300);
} else if (statusCode == HttpStatus.SC_REQUEST_URI_TOO_LONG) {
throw new PinboardException();
} else {
throw new IOException();
}
}
}