/*
* Copyright 2009 Codecarpet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.codecarpet.fbconnect;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import temporary.CcUtil;
import android.graphics.Bitmap;
public class FBRequest {
// /////////////////////////////////////////////////////////////////////////////////////////////////
// global
static String kAPIVersion = "1.0";
static String kAPIFormat = "JSON";
static String kUserAgent = "FacebookConnect";
static String kStringBoundary = "3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f";
static String kEncoding = "UTF-8";
static final long kTimeoutInterval = 180;
// /////////////////////////////////////////////////////////////////////////////////////////////////
private FBSession _session;
private FBRequestDelegate _delegate;
private String _url;
private String _method;
private Object _userInfo;
private Map<String, String> _params;
private Object _data;
private Date _timestamp;
private HttpURLConnection _connection;
private StringBuilder _responseText;
private FBRequest() {
}
public FBRequestDelegate getDelegate() {
return _delegate;
}
/**
* The URL which will be contacted to execute the request.
*/
public String getUrl() {
return _url;
}
/**
* The API method which will be called.
*/
public String getMethod() {
return _method;
}
/**
* An object used by the user of the request to help identify the meaning of the request.
*/
public Object getUserInfo() {
return _userInfo;
}
/**
* The dictionary of parameters to pass to the method.
*
* These values in the dictionary will be converted to strings using the standard Objective-C object-to-string
* conversion facilities.
*/
public Map<String, String> getParams() {
return _params;
}
/**
* The timestamp of when the request was sent to the server.
*/
public Date getTimestamp() {
return _timestamp;
}
// /////////////////////////////////////////////////////////////////////////////////////////////////
// class public
/**
* Creates a new API request for the global session.
*/
public static FBRequest request() {
return requestWithSession(FBSession.getSession());
}
/**
* Creates a new API request for the global session with a delegate.
*/
public static FBRequest requestWithDelegate(FBRequestDelegate delegate) {
return requestWithSession(FBSession.getSession(), delegate);
}
/**
* Creates a new API request for a particular session.
*/
public static FBRequest requestWithSession(FBSession session) {
return new FBRequest().initWithSession(session);
}
/**
* Creates a new API request for the global session with a delegate.
*/
public static FBRequest requestWithSession(FBSession session, FBRequestDelegate delegate) {
FBRequest request = requestWithSession(session);
request._delegate = delegate;
return request;
}
// /////////////////////////////////////////////////////////////////////////////////////////////////
// private
private String md5HexDigest(String input) {
// TODO MD5
return CcUtil.generateMD5(input);
// // const char* str = [input UTF8String];
// char[] result = new char[/* CC_MD5_DIGEST_LENGTH */0];
// // CC_MD5(str, strlen(str), result);
//
// return String.format("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", result[0],
// result[1], result[2], result[3], result[4], result[5], result[6], result[7], result[8], result[9],
// result[10], result[11], result[12], result[13], result[14], result[15]);
}
private boolean isSpecialMethod() {
return _method.equals("facebook.auth.getSession") || _method.equals("facebook.auth.createToken");
}
private String urlForMethod(String method) {
return _session.getApiURL();
}
private String generateGetURL() {
try {
URL parsedURL = new URL(_url);
String queryPrefix = parsedURL.getPath().contains("?") ? "&" : "?";
List<String> pairs = new ArrayList<String>();
for (Entry<String, String> entry : _params.entrySet()) {
pairs.add(entry.getKey() + "=" + entry.getValue());
}
String params = CcUtil.componentsJoinedByString(pairs, "&");
return _url + queryPrefix + params;
} catch (MalformedURLException e) {
e.printStackTrace();
}
return null;
}
private String generateCallId() {
return String.format(Long.toString(System.currentTimeMillis()));
}
private String generateSig() {
StringBuilder joined = new StringBuilder();
List<String> keys = new ArrayList<String>(_params.keySet());
Collections.sort(keys, CcUtil.CASE_INSENSITIVE_COMPARATOR);
for (String obj : keys) {
joined.append(obj);
joined.append("=");
Object value = _params.get(obj);
if (value instanceof String) {
joined.append(value);
}
}
if (isSpecialMethod()) {
if (_session.getApiSecret() != null) {
joined.append(_session.getApiSecret());
}
} else if (_session.getSessionSecret() != null) {
joined.append(_session.getSessionSecret());
} else if (_session.getApiSecret() != null) {
joined.append(_session.getApiSecret());
}
return md5HexDigest(joined.toString());
}
private byte[] generatePostBody() throws UnsupportedEncodingException, IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
String bodyString = "--" + kStringBoundary + "\r\n";
String endLine = "\r\n--" + kStringBoundary + "\r\n";
os.write(bodyString.getBytes(kEncoding));
// write all string parameters from the parameter map
for (Entry<String, String> entry : _params.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
String cd = "Content-Disposition: form-data; name=\"" + key + "\"\r\n\r\n";
os.write(cd.getBytes(kEncoding));
os.write(value.getBytes(kEncoding));
os.write(endLine.getBytes(kEncoding));
}
// write a bitmap value, if one exists
if (_data != null) {
if (_data instanceof Bitmap) {
String cd = "Content-Disposition: form-data; filename=\"photo\"\r\n";
String ct = "Content-Type: image/png\r\n\r\n";
Bitmap image = (Bitmap)_data;
os.write(cd.getBytes(kEncoding));
os.write(ct.getBytes(kEncoding));
image.compress(Bitmap.CompressFormat.PNG, 0, os);
os.write(endLine.getBytes(kEncoding));
} else if (_data instanceof byte[]) {
String cd = "Content-Disposition: form-data; filename=\"data\"\r\n";
String ct = "Content-Type: content/unknown\r\n\r\n";
byte[] data = (byte[])_data;
os.write(cd.getBytes(kEncoding));
os.write(ct.getBytes(kEncoding));
os.write(data);
os.write(endLine.getBytes(kEncoding));
}
}
return os.toByteArray();
}
private Object parseJSONResponse(String data) throws JSONException {
// TODO find some reliable way of creating appropriate JSON API class
if (data.startsWith("[")) {
return new JSONArray(data);
} else {
return new JSONObject(data);
}
}
private void succeedWithResult(Object result) {
if (_delegate != null) {
_delegate.request_didLoad(this, result);
}
}
private void failWithError(Throwable error) {
if (_delegate != null) {
_delegate.request_didFailWithError(this, error);
}
}
private void handleResponseData(String data) {
// FBLOG2(@"DATA: %s", data.bytes);
try {
Object result = parseJSONResponse(data);
// check whether the result is an error
if (result instanceof JSONObject) {
JSONObject jso = (JSONObject)result;
if (jso.has("error_code")) {
int errorCode = jso.getInt("error_code");
String errorMessage = jso.getString("error_msg");
JSONArray args = jso.getJSONArray("request_args");
Map<String, String> map = new HashMap<String, String>();
for (int i = 0; i < args.length(); i++) {
JSONObject arg = args.getJSONObject(i);
map.put(arg.getString("key"), arg.getString("value"));
}
failWithError(new FBRequestError(errorCode, errorMessage, map));
return;
}
}
// not an error, so call delegate
succeedWithResult(result);
} catch (JSONException e) {
failWithError(e);
}
}
public void connect() throws IOException {
// FBLOG(@"Connecting to %@ s%@", _url, _params);
_delegate.requestLoading(this);
String url = (_method != null ? _url : generateGetURL());
if (!url.endsWith("/")) {
url += "/";
}
URL serverUrl = new URL(url);
OutputStream out = null;
InputStream in = null;
try {
_connection = (HttpURLConnection) serverUrl.openConnection();
_connection.setRequestProperty("User-Agent", kUserAgent);
byte[] body = null;
if (_method != null) {
_connection.setRequestMethod("POST");
String contentType = "multipart/form-data; boundary=" + kStringBoundary;
_connection.setRequestProperty("Content-Type", contentType);
body = generatePostBody();
}
_connection.setDoOutput(true);
_connection.connect();
if (body != null) {
out = _connection.getOutputStream();
out.write(body);
}
in = _connection.getInputStream();
_responseText = CcUtil.getResponse(in);
// String prettyResponse = new JSONArray(response).toString(2);
// Log.d(LOG, "Query result: " + prettyResponse);
} finally {
CcUtil.close(in);
CcUtil.close(out);
CcUtil.disconnect(_connection);
}
connectionDidFinishLoading();
_timestamp = new Date();
}
// /////////////////////////////////////////////////////////////////////////////////////////////////
// NSObject
/**
* Creates a new request paired to a session.
*/
FBRequest initWithSession(FBSession session) {
_session = session;
_delegate = null;
_url = null;
_method = null;
_params = null;
_userInfo = null;
_timestamp = null;
_connection = null;
_responseText = null;
return this;
}
public String toString() {
return "<FBRequest " + (_method != null ? _method : _url) + ">";
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// NSURLConnectionDelegate
// private void connection_didReceiveResponse(String response) {
// _responseText = new StringBuilder();
//
// _delegate.didReceiveResponse(response);
// }
//
// private void connection_didReceiveData(String data) {
// _responseText.append(data);
// }
private void connectionDidFinishLoading() {
handleResponseData(_responseText.toString());
_responseText = null;
_connection = null;
}
// private void connection_didFailWithError(HttpURLConnection connection, NSError error) {
// failWithError(error);
//
// _responseText = null;
// _connection = null;
// }
// ////////////////////////////////////////////////////////////////////////////////////////////////
// public
/**
* Indicates if the request has been sent and is awaiting a response.
*/
public boolean loading() {
return _connection != null;
}
/**
* Calls a method on the server asynchronously.
*
* Use this form for API calls with no data parameter.
* The delegate will be called for each stage of the loading process.
*/
public void call(String method, Map<String, String> params) {
callWithAnyData(method, params, null);
}
/**
* Calls a method on the server asynchronously.
*
* This version include an arbitrary byte array of data.
* The delegate will be called for each stage of the loading process.
*/
public void call(String method, Map<String, String> params, byte[] data) {
callWithAnyData(method, params, data);
}
/**
* Calls a method on the server asynchronously.
*
* Include a Bitmap as a data parameter for photo uploads.
* The delegate will be called for each stage of the loading process.
*/
public void call(String method, Map<String, String> params, Bitmap data) {
callWithAnyData(method, params, data);
}
/**
* Calls a method on the server asynchronously.
*
* The delegate will be called for each stage of the loading process.
*/
private void callWithAnyData(String method, Map<String, String> params, Object data) {
_url = urlForMethod(method);
_method = method;
_params = params != null ? new HashMap<String, String>(params) : new HashMap<String, String>();
_data = data;
_params.put("method", _method);
_params.put("api_key", _session.getApiKey());
_params.put("v", kAPIVersion);
_params.put("format", kAPIFormat);
if (!isSpecialMethod()) {
_params.put("session_key", _session.getSessionKey());
_params.put("call_id", generateCallId());
if (_session.getSessionSecret() != null) {
_params.put("ss", "1");
}
}
_params.put("sig", generateSig());
_session.send(this);
}
/**
* Calls a URL on the server asynchronously.
*
* The delegate will be called for each stage of the loading process.
*/
public void post(String url, Map<String, String> params) {
_url = url;
_params = params != null ? new HashMap<String, String>(params) : new HashMap<String, String>();
_session.send(this);
}
/**
* Stops an active request before the response has returned.
*/
public void cancel() {
if (_connection != null) {
// TODO
// _connection.cancel();
_connection = null;
_delegate.requestWasCancelled(this);
}
}
public static abstract class FBRequestDelegate {
/**
* Called just before the request is sent to the server.
*/
protected void requestLoading(FBRequest request) {
}
// /**
// * Called when the server responds and begins to send back data.
// */
// void request_didReceiveResponse(FBRequest request, NSURLResponse response);
/**
* Called when an error prevents the request from completing successfully.
*/
protected void request_didFailWithError(FBRequest request, Throwable error) {
}
/**
* Called when a request returns and its response has been parsed into an object.
*
* The resulting object may be a dictionary, an array, a string, or a number, depending on thee format of the
* API response.
*/
protected void request_didLoad(FBRequest request, Object result) {
}
/**
* Called when the request was cancelled.
*/
protected void requestWasCancelled(FBRequest request) {
}
}
}