/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
* copy, modify, and distribute this software in source code or binary form for use
* in connection with the web services and APIs provided by Facebook.
*
* As with any software that integrates with the Facebook platform, your use of
* this software is subject to the Facebook Developer Principles and Policies
* [http://developers.facebook.com/policy/]. This copyright notice shall be
* included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.facebook;
import com.facebook.internal.FacebookRequestErrorClassification;
import com.facebook.internal.Logger;
import com.facebook.internal.Utility;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Encapsulates the response, successful or otherwise, of a call to the Facebook platform.
*/
public class GraphResponse {
private final HttpURLConnection connection;
private final JSONObject graphObject;
private final JSONArray graphObjectArray;
private final FacebookRequestError error;
private final String rawResponse;
private final GraphRequest request;
/**
* Property name of non-JSON results in the GraphObject. Certain calls to Facebook result in a
* non-JSON response (e.g., the string literal "true" or "false"). To present a consistent way
* of accessing results, these are represented as a GraphObject with a single string property
* with this name.
*/
public static final String NON_JSON_RESPONSE_PROPERTY = "FACEBOOK_NON_JSON_RESULT";
// From v2.1 of the Graph API, write endpoints will now return valid JSON with the result as the
// value for the "success" key
public static final String SUCCESS_KEY = "success";
private static final String CODE_KEY = "code";
private static final String BODY_KEY = "body";
private static final String RESPONSE_LOG_TAG = "Response";
GraphResponse(
GraphRequest request,
HttpURLConnection connection,
String rawResponse,
JSONObject graphObject) {
this(request, connection, rawResponse, graphObject, null, null);
}
GraphResponse(
GraphRequest request,
HttpURLConnection connection,
String rawResponse,
JSONArray graphObjects) {
this(request, connection, rawResponse, null, graphObjects, null);
}
GraphResponse(
GraphRequest request,
HttpURLConnection connection,
FacebookRequestError error) {
this(request, connection, null, null, null, error);
}
GraphResponse(
GraphRequest request,
HttpURLConnection connection,
String rawResponse,
JSONObject graphObject,
JSONArray graphObjects,
FacebookRequestError error) {
this.request = request;
this.connection = connection;
this.rawResponse = rawResponse;
this.graphObject = graphObject;
this.graphObjectArray = graphObjects;
this.error = error;
}
/**
* Returns information about any errors that may have occurred during the request.
*
* @return the error from the server, or null if there was no server error
*/
public final FacebookRequestError getError() {
return error;
}
/**
* The response returned for this request, if it's in object form.
*
* @return the returned JSON object, or null if none was returned (or if the result was a JSON
* array)
*/
public final JSONObject getJSONObject() {
return graphObject;
}
/**
* The response returned for this request, if it's in array form.
*
* @return the returned JSON array, or null if none was returned (or if the result was a JSON
* object)
*/
public final JSONArray getJSONArray() {
return graphObjectArray;
}
/**
* Returns the HttpURLConnection that this response was generated from. If the response was
* retrieved from the cache, this will be null.
*
* @return the connection, or null
*/
public final HttpURLConnection getConnection() {
return connection;
}
/**
* Returns the request that this response is for.
*
* @return the request that this response is for
*/
public GraphRequest getRequest() {
return request;
}
/**
* Returns the server response as a String that this response is for.
*
* @return A String representation of the actual response from the server
*/
public String getRawResponse() {
return rawResponse;
}
/**
* Indicates whether paging is being done forward or backward.
*/
public enum PagingDirection {
/**
* Indicates that paging is being performed in the forward direction.
*/
NEXT,
/**
* Indicates that paging is being performed in the backward direction.
*/
PREVIOUS
}
/**
* If a Response contains results that contain paging information, returns a new
* Request that will retrieve the next page of results, in whichever direction
* is desired. If no paging information is available, returns null.
*
* @param direction enum indicating whether to page forward or backward
* @return a Request that will retrieve the next page of results in the desired
* direction, or null if no paging information is available
*/
public GraphRequest getRequestForPagedResults(PagingDirection direction) {
String link = null;
if (graphObject != null) {
JSONObject pagingInfo = graphObject.optJSONObject("paging");
if (pagingInfo != null) {
if (direction == PagingDirection.NEXT) {
link = pagingInfo.optString("next");
} else {
link = pagingInfo.optString("previous");
}
}
}
if (Utility.isNullOrEmpty(link)) {
return null;
}
if (link != null && link.equals(request.getUrlForSingleRequest())) {
// We got the same "next" link as we just tried to retrieve. This could happen if cached
// data is invalid. All we can do in this case is pretend we have finished.
return null;
}
GraphRequest pagingRequest;
try {
pagingRequest = new GraphRequest(request.getAccessToken(), new URL(link));
} catch (MalformedURLException e) {
return null;
}
return pagingRequest;
}
/**
* Provides a debugging string for this response.
*/
@Override
public String toString() {
String responseCode;
try {
responseCode = String.format(
Locale.US,
"%d",
(connection != null) ? connection.getResponseCode() : 200);
} catch (IOException e) {
responseCode = "unknown";
}
return new StringBuilder()
.append("{Response: ")
.append(" responseCode: ")
.append(responseCode)
.append(", graphObject: ")
.append(graphObject)
.append(", error: ")
.append(error)
.append("}")
.toString();
}
@SuppressWarnings("resource")
static List<GraphResponse> fromHttpConnection(
HttpURLConnection connection,
GraphRequestBatch requests) {
InputStream stream = null;
try {
if (connection.getResponseCode() >= 400) {
stream = connection.getErrorStream();
} else {
stream = connection.getInputStream();
}
return createResponsesFromStream(stream, connection, requests);
} catch (FacebookException facebookException) {
Logger.log(
LoggingBehavior.REQUESTS,
RESPONSE_LOG_TAG,
"Response <Error>: %s",
facebookException);
return constructErrorResponses(requests, connection, facebookException);
} catch (Exception exception) {
// Note due to bugs various android devices some devices can throw a
// SecurityException or NoSuchAlgorithmException. Make sure to handle these
// exceptions here.
Logger.log(
LoggingBehavior.REQUESTS,
RESPONSE_LOG_TAG,
"Response <Error>: %s",
exception);
return constructErrorResponses(requests, connection, new FacebookException(exception));
} finally {
Utility.closeQuietly(stream);
}
}
static List<GraphResponse> createResponsesFromStream(
InputStream stream,
HttpURLConnection connection,
GraphRequestBatch requests
) throws FacebookException, JSONException, IOException {
String responseString = Utility.readStreamToString(stream);
Logger.log(LoggingBehavior.INCLUDE_RAW_RESPONSES, RESPONSE_LOG_TAG,
"Response (raw)\n Size: %d\n Response:\n%s\n", responseString.length(),
responseString);
return createResponsesFromString(responseString, connection, requests);
}
static List<GraphResponse> createResponsesFromString(
String responseString,
HttpURLConnection connection,
GraphRequestBatch requests
) throws FacebookException, JSONException, IOException {
JSONTokener tokener = new JSONTokener(responseString);
Object resultObject = tokener.nextValue();
List<GraphResponse> responses = createResponsesFromObject(
connection,
requests,
resultObject);
Logger.log(
LoggingBehavior.REQUESTS,
RESPONSE_LOG_TAG,
"Response\n Id: %s\n Size: %d\n Responses:\n%s\n",
requests.getId(),
responseString.length(),
responses);
return responses;
}
private static List<GraphResponse> createResponsesFromObject(
HttpURLConnection connection,
List<GraphRequest> requests,
Object object
) throws FacebookException, JSONException {
int numRequests = requests.size();
List<GraphResponse> responses = new ArrayList<GraphResponse>(numRequests);
Object originalResult = object;
if (numRequests == 1) {
GraphRequest request = requests.get(0);
try {
// Single request case -- the entire response is the result, wrap it as "body" so we
// can handle it the same as we do in the batched case. We get the response code
// from the actual HTTP response, as opposed to the batched case where it is
// returned as a "code" element.
JSONObject jsonObject = new JSONObject();
jsonObject.put(BODY_KEY, object);
int responseCode = (connection != null) ? connection.getResponseCode() : 200;
jsonObject.put(CODE_KEY, responseCode);
JSONArray jsonArray = new JSONArray();
jsonArray.put(jsonObject);
// Pretend we got an array of 1 back.
object = jsonArray;
} catch (JSONException e) {
responses.add(
new GraphResponse(
request,
connection,
new FacebookRequestError(connection, e)));
} catch (IOException e) {
responses.add(
new GraphResponse(
request,
connection,
new FacebookRequestError(connection, e)));
}
}
if (!(object instanceof JSONArray) || ((JSONArray) object).length() != numRequests) {
throw new FacebookException("Unexpected number of results");
}
JSONArray jsonArray = (JSONArray) object;
for (int i = 0; i < jsonArray.length(); ++i) {
GraphRequest request = requests.get(i);
try {
Object obj = jsonArray.get(i);
responses.add(
createResponseFromObject(
request,
connection,
obj,
originalResult));
} catch (JSONException e) {
responses.add(
new GraphResponse(
request,
connection,
new FacebookRequestError(connection, e)));
} catch (FacebookException e) {
responses.add(
new GraphResponse(
request,
connection,
new FacebookRequestError(connection, e)));
}
}
return responses;
}
private static GraphResponse createResponseFromObject(
GraphRequest request,
HttpURLConnection connection,
Object object,
Object originalResult
) throws JSONException {
if (object instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) object;
FacebookRequestError error =
FacebookRequestError.checkResponseAndCreateError(
jsonObject,
originalResult,
connection);
if (error != null) {
if (error.getErrorCode() == FacebookRequestErrorClassification.EC_INVALID_TOKEN
&& Utility.isCurrentAccessToken(request.getAccessToken())) {
AccessToken.setCurrentAccessToken(null);
}
return new GraphResponse(request, connection, error);
}
Object body = Utility.getStringPropertyAsJSON(
jsonObject,
BODY_KEY,
NON_JSON_RESPONSE_PROPERTY);
if (body instanceof JSONObject) {
return new GraphResponse(request, connection, body.toString(), (JSONObject)body);
} else if (body instanceof JSONArray) {
return new GraphResponse(request, connection, body.toString(), (JSONArray)body);
}
// We didn't get a body we understand how to handle, so pretend we got nothing.
object = JSONObject.NULL;
}
if (object == JSONObject.NULL) {
return new GraphResponse(request, connection, object.toString(), (JSONObject)null);
} else {
throw new FacebookException("Got unexpected object type in response, class: "
+ object.getClass().getSimpleName());
}
}
static List<GraphResponse> constructErrorResponses(
List<GraphRequest> requests,
HttpURLConnection connection,
FacebookException error) {
int count = requests.size();
List<GraphResponse> responses = new ArrayList<GraphResponse>(count);
for (int i = 0; i < count; ++i) {
GraphResponse response = new GraphResponse(
requests.get(i),
connection,
new FacebookRequestError(connection, error));
responses.add(response);
}
return responses;
}
}