Blame | Last modification | View Log | RSS feed
/*** Copyright 2010-present Facebook.** 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.facebook;import android.content.Context;import com.facebook.internal.CacheableRequestBatch;import com.facebook.internal.FileLruCache;import com.facebook.internal.Logger;import com.facebook.internal.Utility;import com.facebook.model.GraphObject;import com.facebook.model.GraphObjectList;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;/*** Encapsulates the response, successful or otherwise, of a call to the Facebook platform.*/public class Response {private final HttpURLConnection connection;private final GraphObject graphObject;private final GraphObjectList<GraphObject> graphObjectList;private final boolean isFromCache;private final FacebookRequestError error;private final String rawResponse;private final Request 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" keypublic static final String SUCCESS_KEY = "success";private static final int INVALID_SESSION_FACEBOOK_ERROR_CODE = 190;private static final String CODE_KEY = "code";private static final String BODY_KEY = "body";private static final String RESPONSE_LOG_TAG = "Response";private static final String RESPONSE_CACHE_TAG = "ResponseCache";private static FileLruCache responseCache;Response(Request request, HttpURLConnection connection, String rawResponse, GraphObject graphObject, boolean isFromCache) {this(request, connection, rawResponse, graphObject, null, isFromCache, null);}Response(Request request, HttpURLConnection connection, String rawResponse, GraphObjectList<GraphObject> graphObjects,boolean isFromCache) {this(request, connection, rawResponse, null, graphObjects, isFromCache, null);}Response(Request request, HttpURLConnection connection, FacebookRequestError error) {this(request, connection, null, null, null, false, error);}Response(Request request, HttpURLConnection connection, String rawResponse, GraphObject graphObject, GraphObjectList<GraphObject> graphObjects, boolean isFromCache, FacebookRequestError error) {this.request = request;this.connection = connection;this.rawResponse = rawResponse;this.graphObject = graphObject;this.graphObjectList = graphObjects;this.isFromCache = isFromCache;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 single graph object returned for this request, if any.** @return the graph object returned, or null if none was returned (or if the result was a list)*/public final GraphObject getGraphObject() {return graphObject;}/*** The single graph object returned for this request, if any, cast into a particular type of GraphObject.** @param graphObjectClass the GraphObject-derived interface to cast the graph object into* @return the graph object returned, or null if none was returned (or if the result was a list)* @throws FacebookException If the passed in Class is not a valid GraphObject interface*/public final <T extends GraphObject> T getGraphObjectAs(Class<T> graphObjectClass) {if (graphObject == null) {return null;}if (graphObjectClass == null) {throw new NullPointerException("Must pass in a valid interface that extends GraphObject");}return graphObject.cast(graphObjectClass);}/*** The list of graph objects returned for this request, if any.** @return the list of graph objects returned, or null if none was returned (or if the result was not a list)*/public final GraphObjectList<GraphObject> getGraphObjectList() {return graphObjectList;}/*** The list of graph objects returned for this request, if any, cast into a particular type of GraphObject.** @param graphObjectClass the GraphObject-derived interface to cast the graph objects into* @return the list of graph objects returned, or null if none was returned (or if the result was not a list)* @throws FacebookException If the passed in Class is not a valid GraphObject interface*/public final <T extends GraphObject> GraphObjectList<T> getGraphObjectListAs(Class<T> graphObjectClass) {if (graphObjectList == null) {return null;}return graphObjectList.castToListOf(graphObjectClass);}/*** 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 Request 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 Request getRequestForPagedResults(PagingDirection direction) {String link = null;if (graphObject != null) {PagedResults pagedResults = graphObject.cast(PagedResults.class);PagingInfo pagingInfo = pagedResults.getPaging();if (pagingInfo != null) {if (direction == PagingDirection.NEXT) {link = pagingInfo.getNext();} else {link = pagingInfo.getPrevious();}}}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;}Request pagingRequest;try {pagingRequest = new Request(request.getSession(), new URL(link));} catch (MalformedURLException e) {return null;}return pagingRequest;}/*** Provides a debugging string for this response.*/@Overridepublic String toString() {String responseCode;try {responseCode = String.format("%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(", isFromCache:").append(isFromCache).append("}").toString();}/*** Indicates whether the response was retrieved from a local cache or from the server.** @return true if the response was cached locally, false if it was retrieved from the server*/public final boolean getIsFromCache() {return isFromCache;}static FileLruCache getResponseCache() {if (responseCache == null) {Context applicationContext = Session.getStaticContext();if (applicationContext != null) {responseCache = new FileLruCache(applicationContext, RESPONSE_CACHE_TAG, new FileLruCache.Limits());}}return responseCache;}@SuppressWarnings("resource")static List<Response> fromHttpConnection(HttpURLConnection connection, RequestBatch requests) {InputStream stream = null;FileLruCache cache = null;String cacheKey = null;if (requests instanceof CacheableRequestBatch) {CacheableRequestBatch cacheableRequestBatch = (CacheableRequestBatch) requests;cache = getResponseCache();cacheKey = cacheableRequestBatch.getCacheKeyOverride();if (Utility.isNullOrEmpty(cacheKey)) {if (requests.size() == 1) {// Default for single requests is to use the URL.cacheKey = requests.get(0).getUrlForSingleRequest();} else {Logger.log(LoggingBehavior.REQUESTS, RESPONSE_CACHE_TAG,"Not using cache for cacheable request because no key was specified");}}// Try loading from cache. If that fails, load from the network.if (!cacheableRequestBatch.getForceRoundTrip() && cache != null && !Utility.isNullOrEmpty(cacheKey)) {try {stream = cache.get(cacheKey);if (stream != null) {return createResponsesFromStream(stream, null, requests, true);}} catch (FacebookException exception) { // retry via roundtrip below} catch (JSONException exception) {} catch (IOException exception) {} finally {Utility.closeQuietly(stream);}}}// Load from the network, and cache the result if not an error.try {if (connection.getResponseCode() >= 400) {stream = connection.getErrorStream();} else {stream = connection.getInputStream();if ((cache != null) && (cacheKey != null) && (stream != null)) {InputStream interceptStream = cache.interceptAndPut(cacheKey, stream);if (interceptStream != null) {stream = interceptStream;}}}return createResponsesFromStream(stream, connection, requests, false);} catch (FacebookException facebookException) {Logger.log(LoggingBehavior.REQUESTS, RESPONSE_LOG_TAG, "Response <Error>: %s", facebookException);return constructErrorResponses(requests, connection, facebookException);} catch (JSONException exception) {Logger.log(LoggingBehavior.REQUESTS, RESPONSE_LOG_TAG, "Response <Error>: %s", exception);return constructErrorResponses(requests, connection, new FacebookException(exception));} catch (IOException exception) {Logger.log(LoggingBehavior.REQUESTS, RESPONSE_LOG_TAG, "Response <Error>: %s", exception);return constructErrorResponses(requests, connection, new FacebookException(exception));} catch (SecurityException exception) {Logger.log(LoggingBehavior.REQUESTS, RESPONSE_LOG_TAG, "Response <Error>: %s", exception);return constructErrorResponses(requests, connection, new FacebookException(exception));} finally {Utility.closeQuietly(stream);}}static List<Response> createResponsesFromStream(InputStream stream, HttpURLConnection connection,RequestBatch requests, boolean isFromCache) 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, isFromCache);}static List<Response> createResponsesFromString(String responseString, HttpURLConnection connection,RequestBatch requests, boolean isFromCache) throws FacebookException, JSONException, IOException {JSONTokener tokener = new JSONTokener(responseString);Object resultObject = tokener.nextValue();List<Response> responses = createResponsesFromObject(connection, requests, resultObject, isFromCache);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<Response> createResponsesFromObject(HttpURLConnection connection, List<Request> requests,Object object, boolean isFromCache) throws FacebookException, JSONException {assert (connection != null) || isFromCache;int numRequests = requests.size();List<Response> responses = new ArrayList<Response>(numRequests);Object originalResult = object;if (numRequests == 1) {Request 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 Response(request, connection, new FacebookRequestError(connection, e)));} catch (IOException e) {responses.add(new Response(request, connection, new FacebookRequestError(connection, e)));}}if (!(object instanceof JSONArray) || ((JSONArray) object).length() != numRequests) {FacebookException exception = new FacebookException("Unexpected number of results");throw exception;}JSONArray jsonArray = (JSONArray) object;for (int i = 0; i < jsonArray.length(); ++i) {Request request = requests.get(i);try {Object obj = jsonArray.get(i);responses.add(createResponseFromObject(request, connection, obj, isFromCache, originalResult));} catch (JSONException e) {responses.add(new Response(request, connection, new FacebookRequestError(connection, e)));} catch (FacebookException e) {responses.add(new Response(request, connection, new FacebookRequestError(connection, e)));}}return responses;}private static Response createResponseFromObject(Request request, HttpURLConnection connection, Object object,boolean isFromCache, 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() == INVALID_SESSION_FACEBOOK_ERROR_CODE) {Session session = request.getSession();if (session != null) {session.closeAndClearTokenInformation();}}return new Response(request, connection, error);}Object body = Utility.getStringPropertyAsJSON(jsonObject, BODY_KEY, NON_JSON_RESPONSE_PROPERTY);if (body instanceof JSONObject) {GraphObject graphObject = GraphObject.Factory.create((JSONObject) body);return new Response(request, connection, body.toString(), graphObject, isFromCache);} else if (body instanceof JSONArray) {GraphObjectList<GraphObject> graphObjectList = GraphObject.Factory.createList((JSONArray) body, GraphObject.class);return new Response(request, connection, body.toString(), graphObjectList, isFromCache);}// 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 Response(request, connection, object.toString(), (GraphObject)null, isFromCache);} else {throw new FacebookException("Got unexpected object type in response, class: "+ object.getClass().getSimpleName());}}static List<Response> constructErrorResponses(List<Request> requests, HttpURLConnection connection,FacebookException error) {int count = requests.size();List<Response> responses = new ArrayList<Response>(count);for (int i = 0; i < count; ++i) {Response response = new Response(requests.get(i), connection, new FacebookRequestError(connection, error));responses.add(response);}return responses;}interface PagingInfo extends GraphObject {String getNext();String getPrevious();}interface PagedResults extends GraphObject {GraphObjectList<GraphObject> getData();PagingInfo getPaging();}}