/** * Copyright 2012 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 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.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 FacebookException 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 ERROR_KEY = "error"; private static final String ERROR_TYPE_FIELD_KEY = "type"; private static final String ERROR_CODE_FIELD_KEY = "code"; private static final String ERROR_MESSAGE_FIELD_KEY = "message"; private static final String ERROR_CODE_KEY = "error_code"; private static final String ERROR_MSG_KEY = "error_msg"; private static final String ERROR_REASON_KEY = "error_reason"; private static final String RESPONSE_LOG_TAG = "Response"; private static final String RESPONSE_CACHE_TAG = "ResponseCache"; private static FileLruCache responseCache; private Response(Request request, HttpURLConnection connection, GraphObject graphObject, GraphObjectList<GraphObject> graphObjects, boolean isFromCache) { if (graphObject != null && graphObjects != null) { throw new FacebookException("Expected either a graphObject or multiple graphObjects, but not both."); } this.request = request; this.connection = connection; this.graphObject = graphObject; this.graphObjectList = graphObjects; this.isFromCache = isFromCache; this.error = null; } private Response(Request request, HttpURLConnection connection, FacebookException error) { this.request = request; this.connection = connection; this.graphObject = null; this.graphObjectList = null; this.isFromCache = false; this.error = error; } /** * Returns the error returned for this request, if any. * * @return the error encountered, or null if the request succeeded */ public final FacebookException 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; } /** * 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(); } 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; // Try loading from cache. If that fails, load from the network. FileLruCache cache = getResponseCache(); String cacheKey = requests.getCacheKey(); if (!requests.getForceRoundTrip() && (cache != null) && (cacheKey != null)) { 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(LoggingBehaviors.REQUESTS, RESPONSE_LOG_TAG, "Response <Error>: %s", facebookException); return constructErrorResponses(requests, connection, facebookException); } catch (JSONException exception) { Logger.log(LoggingBehaviors.REQUESTS, RESPONSE_LOG_TAG, "Response <Error>: %s", exception); return constructErrorResponses(requests, connection, new FacebookException(exception)); } catch (IOException exception) { Logger.log(LoggingBehaviors.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(LoggingBehaviors.INCLUDE_RAW_RESPONSES, RESPONSE_LOG_TAG, "Response (raw)\n Size: %d\n Response:\n%s\n", responseString.length(), responseString); JSONTokener tokener = new JSONTokener(responseString); Object resultObject = tokener.nextValue(); List<Response> responses = createResponsesFromObject(connection, requests, resultObject, isFromCache); Logger.log(LoggingBehaviors.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); 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 FacebookException(e))); } catch (IOException e) { responses.add(new Response(request, connection, new FacebookException(e))); } } if (!(object instanceof JSONArray) || ((JSONArray) object).length() != numRequests) { FacebookException exception = new FacebookException("TODO 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)); } catch (JSONException e) { responses.add(new Response(request, connection, new FacebookException(e))); } catch (FacebookException e) { responses.add(new Response(request, connection, e)); } } return responses; } private static Response createResponseFromObject(Request request, HttpURLConnection connection, Object object, boolean isFromCache) throws JSONException { if (object instanceof JSONObject) { JSONObject jsonObject = (JSONObject) object; FacebookServiceErrorException exception = checkResponseAndCreateException(jsonObject); if (exception != null) { if (exception.getFacebookErrorCode() == INVALID_SESSION_FACEBOOK_ERROR_CODE) { Session session = request.getSession(); if (session != null) { session.closeAndClearTokenInformation(); } } throw exception; } Object body = Utility.getStringPropertyAsJSON(jsonObject, BODY_KEY, NON_JSON_RESPONSE_PROPERTY); GraphObject graphObject = null; GraphObjectList<GraphObject> graphObjectList = null; if (body instanceof JSONObject) { graphObject = GraphObjectWrapper.createGraphObject((JSONObject) body); } else if (body instanceof JSONArray) { graphObjectList = GraphObjectWrapper.wrapArray((JSONArray) body, GraphObject.class); } return new Response(request, connection, graphObject, graphObjectList, isFromCache); } else if (object == JSONObject.NULL) { return new Response(request, connection, null, null, isFromCache); } else { throw new FacebookException("Got unexpected object type in response, class: " + object.getClass().getSimpleName()); } } private static FacebookServiceErrorException checkResponseAndCreateException(JSONObject jsonObject) { try { if (jsonObject.has(CODE_KEY)) { int responseCode = jsonObject.getInt(CODE_KEY); Object body = Utility.getStringPropertyAsJSON(jsonObject, BODY_KEY, NON_JSON_RESPONSE_PROPERTY); if (body != null && body instanceof JSONObject) { JSONObject jsonBody = (JSONObject) body; // Does this response represent an error from the service? We might get either an "error" // with several sub-properties, or else one or more top-level fields containing error info. String errorType = null; String errorMessage = null; int errorCode = -1; boolean hasError = false; if (jsonBody.has(ERROR_KEY)) { // We assume the error object is correctly formatted. JSONObject error = (JSONObject) Utility.getStringPropertyAsJSON(jsonBody, ERROR_KEY, null); errorType = error.optString(ERROR_TYPE_FIELD_KEY, null); errorMessage = error.optString(ERROR_MESSAGE_FIELD_KEY, null); errorCode = error.optInt(ERROR_CODE_FIELD_KEY, -1); hasError = true; } else if (jsonBody.has(ERROR_CODE_KEY) || jsonBody.has(ERROR_MSG_KEY) || jsonBody.has(ERROR_REASON_KEY)) { errorType = jsonBody.optString(ERROR_REASON_KEY, null); errorMessage = jsonBody.optString(ERROR_MSG_KEY, null); errorCode = jsonBody.optInt(ERROR_CODE_KEY, -1); hasError = true; } if (hasError) { return new FacebookServiceErrorException(responseCode, errorCode, errorType, errorMessage, jsonBody); } } // If we didn't get error details, but we did get a failure response code, report it. if (responseCode < 200 || responseCode >= 300) { return new FacebookServiceErrorException(responseCode); } } } catch (JSONException e) { } return null; } 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, error); responses.add(response); } return responses; } }