//------------------------------------------------------------------------------
// Copyright (c) 2012 Microsoft Corporation. All rights reserved.
//
// Description: See the class level JavaDoc comments.
//------------------------------------------------------------------------------
package com.microsoft.live;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AUTH;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.BasicHeader;
import org.json.JSONException;
import org.json.JSONObject;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
/**
* ApiRequest is an abstract base class that represents an Http Request made by the API.
* It does most of the Http Request work inside of the execute method, and provides a
* an abstract factory method for subclasses to choose the type of Http Request to be
* created.
*/
abstract class ApiRequest<ResponseType> {
public interface Observer {
public void onComplete(HttpResponse response);
}
public enum Redirects {
SUPPRESS {
@Override
protected void setQueryParameterOn(UriBuilder builder) {
Redirects.setQueryParameterOn(builder, Boolean.TRUE);
}
}, UNSUPPRESSED {
@Override
protected void setQueryParameterOn(UriBuilder builder) {
Redirects.setQueryParameterOn(builder, Boolean.FALSE);
}
};
/**
* Sets the suppress_redirects query parameter by removing all existing ones
* and then appending it on the given UriBuilder.
*/
protected abstract void setQueryParameterOn(UriBuilder builder);
private static void setQueryParameterOn(UriBuilder builder, Boolean value) {
// The Live SDK is designed to use our value of suppress_redirects.
// If it uses any other value it could cause issues. Remove any previously
// existing suppress_redirects and use ours.
builder.removeQueryParametersWithKey(QueryParameters.SUPPRESS_REDIRECTS);
builder.appendQueryParameter(QueryParameters.SUPPRESS_REDIRECTS, value.toString());
}
}
public enum ResponseCodes {
SUPPRESS {
@Override
protected void setQueryParameterOn(UriBuilder builder) {
ResponseCodes.setQueryParameterOn(builder, Boolean.TRUE);
}
}, UNSUPPRESSED {
@Override
protected void setQueryParameterOn(UriBuilder builder) {
ResponseCodes.setQueryParameterOn(builder, Boolean.FALSE);
}
};
/**
* Sets the suppress_response_codes query parameter by removing all existing ones
* and then appending it on the given UriBuilder.
*/
protected abstract void setQueryParameterOn(UriBuilder builder);
private static void setQueryParameterOn(UriBuilder builder, Boolean value) {
// The Live SDK is designed to use our value of suppress_response_codes.
// If it uses any other value it could cause issues. Remove any previously
// existing suppress_response_codes and use ours.
builder.removeQueryParametersWithKey(QueryParameters.SUPPRESS_RESPONSE_CODES);
builder.appendQueryParameter(QueryParameters.SUPPRESS_RESPONSE_CODES, value.toString());
}
}
private static final Header LIVE_LIBRARY_HEADER =
new BasicHeader("X-HTTP-Live-Library", "android/" + Build.VERSION.RELEASE + "_" +
Config.INSTANCE.getApiVersion());
private static final int SESSION_REFRESH_BUFFER_SECS = 30;
private static final int SESSION_TOKEN_SEND_BUFFER_SECS = 3;
/**
* Constructs a new instance of a Header that contains the
* @param accessToken to construct inside the Authorization header
* @return a new instance of a Header that contains the Authorization access_token
*/
private static Header createAuthroizationHeader(LiveConnectSession session) {
assert session != null;
String accessToken = session.getAccessToken();
assert !TextUtils.isEmpty(accessToken);
String tokenType = OAuth.TokenType.BEARER.toString().toLowerCase();
String value = TextUtils.join(" ", new String[]{tokenType, accessToken});
return new BasicHeader(AUTH.WWW_AUTH_RESP, value);
}
private final HttpClient client;
private final List<Observer> observers;
private final String path;
private final ResponseHandler<ResponseType> responseHandler;
private final LiveConnectSession session;
protected final UriBuilder requestUri;
/** The original path string parsed into a Uri object. */
protected final Uri pathUri;
public ApiRequest(LiveConnectSession session,
HttpClient client,
ResponseHandler<ResponseType> responseHandler,
String path) {
this(session, client, responseHandler, path, ResponseCodes.SUPPRESS, Redirects.SUPPRESS);
}
/**
* Constructs a new instance of an ApiRequest and initializes its member variables
*
* @param session that contains the access_token
* @param client to make Http Requests on
* @param responseHandler to handle the response
* @param path of the request. it can be relative or absolute.
*/
public ApiRequest(LiveConnectSession session,
HttpClient client,
ResponseHandler<ResponseType> responseHandler,
String path,
ResponseCodes responseCodes,
Redirects redirects) {
assert session != null;
assert client != null;
assert responseHandler != null;
assert !TextUtils.isEmpty(path);
this.session = session;
this.client = client;
this.observers = new ArrayList<Observer>();
this.responseHandler = responseHandler;
this.path = path;
UriBuilder builder;
this.pathUri = Uri.parse(path);
if (this.pathUri.isAbsolute()) {
// if the path is absolute we will just use that entire path
builder = UriBuilder.newInstance(this.pathUri);
} else {
// if it is a relative path then we should use the config's API URI,
// which is usually something like https://apis.live.net/v5.0
builder = UriBuilder.newInstance(Config.INSTANCE.getApiUri())
.appendToPath(this.pathUri.getEncodedPath())
.query(this.pathUri.getQuery());
}
responseCodes.setQueryParameterOn(builder);
redirects.setQueryParameterOn(builder);
this.requestUri = builder;
}
public void addObserver(Observer observer) {
this.observers.add(observer);
}
/**
* Performs the Http Request and returns the response from the server
*
* @return an instance of ResponseType from the server
* @throws LiveOperationException if there was an error executing the HttpRequest
*/
public ResponseType execute() throws LiveOperationException {
// Let subclass decide which type of request to instantiate
HttpUriRequest request = this.createHttpRequest();
request.addHeader(LIVE_LIBRARY_HEADER);
if (this.session.willExpireInSecs(SESSION_REFRESH_BUFFER_SECS)) {
this.session.refresh();
}
// if the session will soon expire, try to send the request without a token.
// the request *may* not need the token, let's give it a try rather than
// risk a request with an invalid token.
if (!this.session.willExpireInSecs(SESSION_TOKEN_SEND_BUFFER_SECS)) {
request.addHeader(createAuthroizationHeader(this.session));
}
try {
HttpResponse response = this.client.execute(request);
for (Observer observer : this.observers) {
observer.onComplete(response);
}
return this.responseHandler.handleResponse(response);
} catch (ClientProtocolException e) {
throw new LiveOperationException(ErrorMessages.SERVER_ERROR, e);
} catch (IOException e) {
// The IOException could contain a JSON object body
// (see InputStreamResponseHandler.java). If it does,
// we want to throw an exception with its message. If it does not, we want to wrap
// the IOException.
try {
new JSONObject(e.getMessage());
throw new LiveOperationException(e.getMessage());
} catch (JSONException jsonException) {
throw new LiveOperationException(ErrorMessages.SERVER_ERROR, e);
}
}
}
/** @return the HTTP method being performed by the request */
public abstract String getMethod();
/** @return the path of the request */
public String getPath() {
return this.path;
}
public void removeObserver(Observer observer) {
this.observers.remove(observer);
}
/**
* Factory method that allows subclasses to choose which type of request will
* be performed.
*
* @return the HttpRequest to perform
* @throws LiveOperationException if there is an error creating the HttpRequest
*/
protected abstract HttpUriRequest createHttpRequest() throws LiveOperationException;
}