/** * 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.*; 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 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"; 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, GraphObject graphObject, boolean isFromCache) { this.request = request; this.connection = connection; this.graphObject = graphObject; this.graphObjectList = null; this.isFromCache = isFromCache; this.error = null; } Response(Request request, HttpURLConnection connection, GraphObjectList<GraphObject> graphObjects, boolean isFromCache) { this.request = request; this.connection = connection; this.graphObject = null; this.graphObjectList = graphObjects; this.isFromCache = isFromCache; this.error = null; } Response(Request request, HttpURLConnection connection, FacebookRequestError error) { this.request = request; this.connection = connection; this.graphObject = null; this.graphObjectList = null; this.isFromCache = false; 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; } /** * 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. */ @Override public 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, graphObject, isFromCache); } else if (body instanceof JSONArray) { GraphObjectList<GraphObject> graphObjectList = GraphObject.Factory.createList( (JSONArray) body, GraphObject.class); return new Response(request, connection, 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, (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(); } }