Java tutorial
/* * Copyright (c) 2010-2012 Mark Allen. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission 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.restfb; import com.google.common.base.Function; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.restfb.WebRequestor.Response; import com.restfb.batch.BatchRequest; import com.restfb.batch.BatchResponse; import com.restfb.exception.*; import com.restfb.json.JsonArray; import com.restfb.json.JsonException; import com.restfb.json.JsonObject; import javax.annotation.Nullable; import java.io.IOException; import java.util.*; import java.util.concurrent.ExecutionException; import static com.restfb.util.StringUtils.*; import static com.restfb.util.UrlUtils.urlEncode; import static java.lang.String.format; import static java.net.HttpURLConnection.*; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; /** * Default implementation of a <a href="http://developers.facebook.com/docs/api">Facebook Graph API</a> client. * * @author <a href="http://restfb.com">Mark Allen</a> */ public class DefaultFacebookClient extends BaseFacebookClient implements FacebookClient, AsyncFacebookClient { /** * Handles asynchronous {@code GET}s and {@code POST}s to the Facebook API endpoint. */ protected WebRequestor syncWebRequestor; /** * {@link DefaultFacebookClient#syncWebRequestor} wrapped as {@link AsyncWebRequestor} */ protected AsyncWebRequestor syncWebRequestorAsAsync; /** * Handles asynchronous {@code GET}s and {@code POST}s to the Facebook API endpoint. */ protected AsyncWebRequestor asyncWebRequestor; /** * Graph API access token. */ protected String accessToken; /** * Knows how to map Graph API exceptions to formal Java exception types. */ protected FacebookExceptionMapper graphFacebookExceptionMapper; /** * API endpoint URL. */ protected static final String FACEBOOK_GRAPH_ENDPOINT_URL = "https://graph.facebook.com"; /** * Read-only API endpoint URL. */ protected static final String FACEBOOK_READ_ONLY_ENDPOINT_URL = "https://api-read.facebook.com/method"; /** * Video Upload API endpoint URL. * * @since 1.6.5 */ protected static final String FACEBOOK_GRAPH_VIDEO_ENDPOINT_URL = "https://graph-video.facebook.com"; /** * Reserved method override parameter name. */ protected static final String METHOD_PARAM_NAME = "method"; /** * Reserved "multiple IDs" parameter name. */ protected static final String IDS_PARAM_NAME = "ids"; /** * Reserved FQL query parameter name. */ protected static final String QUERY_PARAM_NAME = "query"; /** * Reserved FQL multiquery parameter name. */ protected static final String QUERIES_PARAM_NAME = "queries"; /** * Reserved "result format" parameter name. */ protected static final String FORMAT_PARAM_NAME = "format"; /** * API error response 'error' attribute name. */ protected static final String ERROR_ATTRIBUTE_NAME = "error"; /** * API error response 'type' attribute name. */ protected static final String ERROR_TYPE_ATTRIBUTE_NAME = "type"; /** * API error response 'message' attribute name. */ protected static final String ERROR_MESSAGE_ATTRIBUTE_NAME = "message"; /** * API error response 'code' attribute name. */ protected static final String ERROR_CODE_ATTRIBUTE_NAME = "code"; /** * Batch API error response 'error' attribute name. */ protected static final String BATCH_ERROR_ATTRIBUTE_NAME = "error"; /** * Batch API error response 'error_description' attribute name. */ protected static final String BATCH_ERROR_DESCRIPTION_ATTRIBUTE_NAME = "error_description"; /** * Creates a Facebook Graph API client with no access token. * <p> * Without an access token, you can view and search public graph data but can't do much else. */ public DefaultFacebookClient() { this(null); } /** * Creates a Facebook Graph API client with the given {@code accessToken}. * * @param accessToken * A Facebook OAuth access token. */ public DefaultFacebookClient(String accessToken) { this(accessToken, new DefaultWebRequestor(), null, new DefaultJsonMapper()); } /** * Creates a Facebook Graph API client with the given {@code accessToken}, {@code syncWebRequestor}, and * {@code jsonMapper}. * * @param accessToken * A Facebook OAuth access token. * @param syncWebRequestor * The {@link WebRequestor} implementation to use for sending requests to the API endpoint. * If set to null synchronous functionality of client will be disabled. * @param syncWebRequestor * The {@link AsyncWebRequestor} implementation to use for sending asynchronous requests to the API endpoint. * If set to null asynchronous functionality of client will be disabled. * @param jsonMapper * The {@link JsonMapper} implementation to use for mapping API response JSON to Java objects. * @throws NullPointerException * If {@code jsonMapper} or {@code syncWebRequestor} is {@code null}. */ public DefaultFacebookClient(String accessToken, WebRequestor syncWebRequestor, AsyncWebRequestor asyncWebRequestor, JsonMapper jsonMapper) { super(); verifyParameterPresence("jsonMapper", jsonMapper); if (syncWebRequestor == null && asyncWebRequestor == null) { throw new IllegalArgumentException("either sync or asyn web requestor must be not null"); } this.accessToken = trimToNull(accessToken); this.syncWebRequestor = syncWebRequestor; if (syncWebRequestor != null) { syncWebRequestorAsAsync = new SyncToAsyncWebRequestor(syncWebRequestor); } this.asyncWebRequestor = asyncWebRequestor; this.jsonMapper = jsonMapper; graphFacebookExceptionMapper = createGraphFacebookExceptionMapper(); illegalParamNames.addAll( Arrays.asList(new String[] { ACCESS_TOKEN_PARAM_NAME, METHOD_PARAM_NAME, FORMAT_PARAM_NAME })); } /** * @see com.restfb.FacebookClient#deleteObject(java.lang.String, com.restfb.Parameter[]) */ @Override public boolean deleteObject(String object, Parameter... parameters) { verifyParameterPresence("object", object); return "true".equals(makeRequest(object, true, true, null, parameters)); } /** * @see com.restfb.FacebookClient#fetchConnection(java.lang.String, java.lang.Class, com.restfb.Parameter[]) */ public <T> Connection<T> fetchConnection(String connection, Class<T> connectionType, Parameter... parameters) { verifyParameterPresence("connection", connection); verifyParameterPresence("connectionType", connectionType); return new Connection<T>(this, makeRequest(connection, parameters), connectionType); } /** * @see com.restfb.FacebookClient#fetchConnectionPage(java.lang.String, java.lang.Class) */ public <T> Connection<T> fetchConnectionPage(final String connectionPageUrl, Class<T> connectionType) { verifySyncWebRequestor(); String connectionJson; try { connectionJson = makeRequestFullEndpoint(syncWebRequestorAsAsync, connectionPageUrl, null, false, null) .get(); } catch (InterruptedException e) { throw new FacebookNetworkException("request interrupted", e); } catch (ExecutionException e) { assert e.getCause() instanceof FacebookException; throw (FacebookException) e.getCause(); } return new Connection<T>(this, connectionJson, connectionType); } /** * @see com.restfb.FacebookClient#fetchObject(java.lang.String, java.lang.Class, com.restfb.Parameter[]) */ public <T> T fetchObject(String object, Class<T> objectType, Parameter... parameters) { return facebookFutureGet(fetchObject(syncWebRequestorAsAsync, object, objectType, parameters)); } @Override public <T> ListenableFuture<T> asyncFetchObject(String object, Class<T> objectType, Parameter... parameters) { verifyAsyncWebRequestor(); return fetchObject(asyncWebRequestor, object, objectType, parameters); } private <T> ListenableFuture<T> fetchObject(AsyncWebRequestor webRequestor, String object, final Class<T> objectType, Parameter... parameters) { verifyParameterPresence("object", object); verifyParameterPresence("objectType", objectType); return transformJsonToJavaObject(makeRequest(webRequestor, object, parameters), objectType); } /** * @see com.restfb.FacebookClient#fetchObjects(java.util.List, java.lang.Class, com.restfb.Parameter[]) */ @SuppressWarnings("unchecked") public <T> T fetchObjects(List<String> ids, Class<T> objectType, Parameter... parameters) { verifyParameterPresence("ids", ids); verifyParameterPresence("connectionType", objectType); if (ids.size() == 0) throw new IllegalArgumentException("The list of IDs cannot be empty."); for (Parameter parameter : parameters) if (IDS_PARAM_NAME.equals(parameter.name)) throw new IllegalArgumentException("You cannot specify the '" + IDS_PARAM_NAME + "' URL parameter yourself - " + "RestFB will populate this for you with " + "the list of IDs you passed to this method."); // Normalize the IDs for (int i = 0; i < ids.size(); i++) { String id = ids.get(i).trim().toLowerCase(); if ("".equals(id)) throw new IllegalArgumentException("The list of IDs cannot contain blank strings."); ids.set(i, id); } try { JsonObject jsonObject = new JsonObject(makeRequest("", parametersWithAdditionalParameter(Parameter.with(IDS_PARAM_NAME, join(ids)), parameters))); return objectType.equals(JsonObject.class) ? (T) jsonObject : jsonMapper.toJavaObject(jsonObject.toString(), objectType); } catch (JsonException e) { throw new FacebookJsonMappingException("Unable to map connection JSON to Java objects", e); } } /** * @see com.restfb.FacebookClient#publish(java.lang.String, java.lang.Class, com.restfb.BinaryAttachment, * com.restfb.Parameter[]) */ public <T> T publish(String connection, Class<T> objectType, BinaryAttachment binaryAttachment, Parameter... parameters) { verifyParameterPresence("connection", connection); List<BinaryAttachment> binaryAttachments = new ArrayList<BinaryAttachment>(); if (binaryAttachment != null) binaryAttachments.add(binaryAttachment); return jsonMapper.toJavaObject(makeRequest(connection, true, false, binaryAttachments, parameters), objectType); } /** * @see com.restfb.FacebookClient#publish(java.lang.String, java.lang.Class, com.restfb.Parameter[]) */ public <T> T publish(String connection, Class<T> objectType, Parameter... parameters) { return publish(connection, objectType, null, parameters); } /** * @see com.restfb.FacebookClient#executeMultiquery(java.util.Map, java.lang.Class, com.restfb.Parameter[]) */ @SuppressWarnings("unchecked") public <T> T executeMultiquery(Map<String, String> queries, Class<T> objectType, Parameter... parameters) { verifyParameterPresence("objectType", objectType); for (Parameter parameter : parameters) if (QUERIES_PARAM_NAME.equals(parameter.name)) throw new IllegalArgumentException("You cannot specify the '" + QUERIES_PARAM_NAME + "' URL parameter yourself - " + "RestFB will populate this for you with " + "the queries you passed to this method."); try { JsonArray jsonArray = new JsonArray( makeRequest("fql.multiquery", false, false, null, parametersWithAdditionalParameter( Parameter.with(QUERIES_PARAM_NAME, queriesToJson(queries)), parameters))); JsonObject normalizedJson = new JsonObject(); for (int i = 0; i < jsonArray.length(); i++) { JsonObject jsonObject = jsonArray.getJsonObject(i); // For empty resultsets, Facebook will return an empty object instead of // an empty list. Hack around that here. JsonArray resultsArray = jsonObject.get("fql_result_set") instanceof JsonArray ? jsonObject.getJsonArray("fql_result_set") : new JsonArray(); normalizedJson.put(jsonObject.getString("name"), resultsArray); } return objectType.equals(JsonObject.class) ? (T) normalizedJson : jsonMapper.toJavaObject(normalizedJson.toString(), objectType); } catch (JsonException e) { throw new FacebookJsonMappingException("Unable to process fql.multiquery JSON response", e); } } /** * @see com.restfb.FacebookClient#executeQuery(java.lang.String, java.lang.Class, com.restfb.Parameter[]) */ public <T> List<T> executeQuery(String query, Class<T> objectType, Parameter... parameters) { verifyParameterPresence("query", query); verifyParameterPresence("objectType", objectType); for (Parameter parameter : parameters) if (QUERY_PARAM_NAME.equals(parameter.name)) throw new IllegalArgumentException("You cannot specify the '" + QUERY_PARAM_NAME + "' URL parameter yourself - " + "RestFB will populate this for you with " + "the query you passed to this method."); return jsonMapper.toJavaList( makeRequest("fql.query", false, false, null, parametersWithAdditionalParameter(Parameter.with(QUERY_PARAM_NAME, query), parameters)), objectType); } /** * @see com.restfb.FacebookClient#executeBatch(com.restfb.batch.BatchRequest[]) */ public List<BatchResponse> executeBatch(BatchRequest... batchRequests) { return executeBatch(asList(batchRequests), Collections.<BinaryAttachment>emptyList()); } /** * @see com.restfb.FacebookClient#executeBatch(java.util.List, java.util.List) */ public List<BatchResponse> executeBatch(List<BatchRequest> batchRequests, List<BinaryAttachment> binaryAttachments) { verifyParameterPresence("binaryAttachments", binaryAttachments); if (batchRequests == null || batchRequests.size() == 0) throw new IllegalArgumentException("You must specify at least one batch request."); return jsonMapper.toJavaList(makeRequest("", true, false, binaryAttachments, Parameter.with("batch", jsonMapper.toJson(batchRequests, true))), BatchResponse.class); } /** * @see com.restfb.FacebookClient#convertSessionKeysToAccessTokens(java.lang.String, java.lang.String, * java.lang.String[]) */ public List<AccessToken> convertSessionKeysToAccessTokens(String appId, String secretKey, String... sessionKeys) { verifyParameterPresence("appId", appId); verifyParameterPresence("secretKey", secretKey); if (sessionKeys == null || sessionKeys.length == 0) return emptyList(); String json = makeRequest("/oauth/exchange_sessions", true, false, null, Parameter.with("client_id", appId), Parameter.with("client_secret", secretKey), Parameter.with("sessions", join(sessionKeys))); return jsonMapper.toJavaList(json, AccessToken.class); } /** * @see com.restfb.FacebookClient#obtainAppAccessToken(java.lang.String, java.lang.String) */ public AccessToken obtainAppAccessToken(String appId, String appSecret) { verifyParameterPresence("appId", appId); verifyParameterPresence("appSecret", appSecret); String response = makeRequest("oauth/access_token", Parameter.with("grant_type", "client_credentials"), Parameter.with("client_id", appId), Parameter.with("client_secret", appSecret)); try { return AccessToken.fromQueryString(response); } catch (Throwable t) { throw new FacebookResponseContentException("Unable to extract access token from response.", t); } } /** * Convenience method which invokes {@link #obtainExtendedAccessToken(String, String, String)} with the current access * token. * * @param appId * The ID of the app for which you'd like to obtain an extended access token. * @param appSecret * The secret for the app for which you'd like to obtain an extended access token. * @return An extended access token for the given {@code accessToken}. * @throws FacebookException * If an error occurs while attempting to obtain an extended access token. * @throws IllegalStateException * If this instance was not constructed with an access token. * @since 1.6.10 */ public AccessToken obtainExtendedAccessToken(String appId, String appSecret) { if (accessToken == null) throw new IllegalStateException(format( "You cannot call this method because you did not construct this instance of %s with an access token.", getClass().getSimpleName())); return obtainExtendedAccessToken(appId, appSecret, accessToken); } /** * @see com.restfb.FacebookClient#obtainExtendedAccessToken(java.lang.String, java.lang.String, java.lang.String) */ @Override public AccessToken obtainExtendedAccessToken(String appId, String appSecret, String accessToken) { verifyParameterPresence("appId", appId); verifyParameterPresence("appSecret", appSecret); verifyParameterPresence("accessToken", accessToken); String response = makeRequest("/oauth/access_token", true, false, null, Parameter.with("client_id", appId), Parameter.with("client_secret", appSecret), Parameter.with("grant_type", "fb_exchange_token"), Parameter.with("fb_exchange_token", accessToken)); try { return AccessToken.fromQueryString(response); } catch (Throwable t) { throw new FacebookResponseContentException("Unable to extract access token from response.", t); } } /** * @see com.restfb.FacebookClient#getJsonMapper() */ public JsonMapper getJsonMapper() { return jsonMapper; } /** * @see com.restfb.FacebookClient#getWebRequestor() */ public WebRequestor getWebRequestor() { return syncWebRequestor; } /** * Synchronous version of {@link #makeRequest(AsyncWebRequestor, String, Parameter...)} */ protected String makeRequest(String endpoint, Parameter... parameters) { return makeRequest(endpoint, false, false, null, parameters); } /** * Coordinates the process of executing the API request GET/POST and processing the response we receive from the * endpoint. * * @param endpoint * Facebook Graph API endpoint. * @param parameters * Arbitrary number of parameters to send along to Facebook as part of the API call. * @return The JSON returned by Facebook for the API call. * @throws FacebookException * If an error occurs while making the Facebook API POST or processing the response. */ protected ListenableFuture<String> makeRequest(AsyncWebRequestor webRequestor, String endpoint, Parameter... parameters) { return makeRequest(webRequestor, endpoint, false, false, null, parameters); } /** * Coordinates the process of executing the API request GET/POST and processing the response we receive from the * endpoint. * * @param endpoint * Facebook Graph API endpoint. * @param executeAsPost * {@code true} to execute the web request as a {@code POST}, {@code false} to execute as a {@code GET}. * @param executeAsDelete * {@code true} to add a special 'treat this request as a {@code DELETE}' parameter. * @param binaryAttachments * A binary file to include in a {@code POST} request. Pass {@code null} if no attachment should be sent. * @param parameters * Arbitrary number of parameters to send along to Facebook as part of the API call. * @return The JSON returned by Facebook for the API call. * @throws FacebookException * If an error occurs while making the Facebook API POST or processing the response. */ protected String makeRequest(String endpoint, final boolean executeAsPost, boolean executeAsDelete, final List<BinaryAttachment> binaryAttachments, Parameter... parameters) { verifySyncWebRequestor(); try { return makeRequest(syncWebRequestorAsAsync, endpoint, executeAsPost, executeAsDelete, binaryAttachments, parameters).get(); } catch (InterruptedException e) { throw new FacebookNetworkException("request interrupted", e); } catch (ExecutionException e) { assert e.getCause() instanceof FacebookException; throw (FacebookException) e.getCause(); } } /** * Coordinates the process of executing the API request GET/POST and processing the response we receive from the * endpoint. * * @param endpoint * Facebook Graph API endpoint. * @param executeAsPost * {@code true} to execute the web request as a {@code POST}, {@code false} to execute as a {@code GET}. * @param executeAsDelete * {@code true} to add a special 'treat this request as a {@code DELETE}' parameter. * @param binaryAttachments * A binary file to include in a {@code POST} request. Pass {@code null} if no attachment should be sent. * @param parameters * Arbitrary number of parameters to send along to Facebook as part of the API call. * @return The JSON returned by Facebook for the API call. * @throws FacebookException * If an error occurs while making the Facebook API POST or processing the response. */ protected ListenableFuture<String> makeRequest(final AsyncWebRequestor webRequestor, String endpoint, final boolean executeAsPost, boolean executeAsDelete, final List<BinaryAttachment> binaryAttachments, Parameter... parameters) { verifySyncWebRequestor(); verifyParameterLegality(parameters); if (executeAsDelete) parameters = parametersWithAdditionalParameter(Parameter.with(METHOD_PARAM_NAME, "delete"), parameters); trimToEmpty(endpoint).toLowerCase(); if (!endpoint.startsWith("/")) endpoint = "/" + endpoint; final String fullEndpoint = createEndpointForApiCall(endpoint, binaryAttachments != null && binaryAttachments.size() > 0); final String parameterString = toParameterString(parameters); return makeRequestFullEndpoint(webRequestor, fullEndpoint, parameterString, executeAsPost, binaryAttachments); } private void verifySyncWebRequestor() { if (syncWebRequestor == null) { throw new IllegalStateException("synchronous webRequestor not set; cannot make request"); } } private void verifyAsyncWebRequestor() { if (asyncWebRequestor == null) { throw new IllegalStateException("asynchronous webRequestor not set; cannot make request"); } } private ListenableFuture<String> makeRequestFullEndpoint(final AsyncWebRequestor webRequestor, final String fullEndpoint, final String parameterString, final boolean executeAsPost, final List<BinaryAttachment> binaryAttachments) { return makeRequestAndProcessResponse(new Requestor() { /** * @see com.restfb.DefaultFacebookClient.Requestor#makeRequest() */ public ListenableFuture<Response> makeRequest() throws IOException { return executeAsPost ? webRequestor.executePost(fullEndpoint, parameterString, binaryAttachments == null ? null : binaryAttachments.toArray(new BinaryAttachment[] {})) : webRequestor.executeGet( parameterString != null ? fullEndpoint + "?" + parameterString : fullEndpoint); } }); } protected static interface Requestor { ListenableFuture<Response> makeRequest() throws IOException; } protected ListenableFuture<String> makeRequestAndProcessResponse(Requestor requestor) { ListenableFuture<Response> responseFuture = null; // Perform a GET or POST to the API endpoint try { responseFuture = requestor.makeRequest(); } catch (Throwable t) { return Futures.immediateFailedFuture(new FacebookNetworkException("Facebook request failed", t)); } final SettableFuture<String> resultStringFuture = SettableFuture.create(); Futures.addCallback(responseFuture, new FutureCallback<Response>() { @Override public void onSuccess(Response response) { // If we get any HTTP response code other than a 200 OK or 400 Bad Request // or 401 Not Authorized or 403 Forbidden or 404 Not Found or 500 Internal Server Error, // throw an exception. if (HTTP_OK != response.getStatusCode() && HTTP_BAD_REQUEST != response.getStatusCode() && HTTP_UNAUTHORIZED != response.getStatusCode() && HTTP_NOT_FOUND != response.getStatusCode() && HTTP_INTERNAL_ERROR != response.getStatusCode() && HTTP_FORBIDDEN != response.getStatusCode()) throw new FacebookNetworkException("Facebook request failed", response.getStatusCode()); String json = response.getBody(); // If the response contained an error code, throw an exception. throwFacebookResponseStatusExceptionIfNecessary(json, response.getStatusCode()); // If there was no response error information and this was a 500 or 401 // error, something weird happened on Facebook's end. Bail. if (HTTP_INTERNAL_ERROR == response.getStatusCode() || HTTP_UNAUTHORIZED == response.getStatusCode()) throw new FacebookNetworkException("Facebook request failed", response.getStatusCode()); resultStringFuture.set(json); } @Override public void onFailure(Throwable t) { if (t instanceof FacebookException) { resultStringFuture.setException(t); } else { resultStringFuture.setException(new FacebookNetworkException("Facebook request failed", t)); } } }); return resultStringFuture; } /** * Throws an exception if Facebook returned an error response. Using the Graph API, it's possible to see both the new * Graph API-style errors as well as Legacy API-style errors, so we have to handle both here. This method extracts * relevant information from the error JSON and throws an exception which encapsulates it for end-user consumption. * <p> * For Graph API errors: * <p> * If the {@code error} JSON field is present, we've got a response status error for this API call. * <p> * For Legacy errors (e.g. FQL): * <p> * If the {@code error_code} JSON field is present, we've got a response status error for this API call. * * @param json * The JSON returned by Facebook in response to an API call. * @param httpStatusCode * The HTTP status code returned by the server, e.g. 500. * @throws FacebookGraphException * If the JSON contains a Graph API error response. * @throws FacebookResponseStatusException * If the JSON contains an Legacy API error response. * @throws FacebookJsonMappingException * If an error occurs while processing the JSON. */ protected void throwFacebookResponseStatusExceptionIfNecessary(String json, Integer httpStatusCode) { // If we have a legacy exception, throw it. throwLegacyFacebookResponseStatusExceptionIfNecessary(json, httpStatusCode); // If we have a batch API exception, throw it. throwBatchFacebookResponseStatusExceptionIfNecessary(json, httpStatusCode); try { // If the result is not an object, bail immediately. if (!json.startsWith("{")) return; JsonObject errorObject = new JsonObject(json); if (errorObject == null || !errorObject.has(ERROR_ATTRIBUTE_NAME)) return; JsonObject innerErrorObject = errorObject.getJsonObject(ERROR_ATTRIBUTE_NAME); // If there's an Integer error code, pluck it out. Integer errorCode = innerErrorObject.has(ERROR_CODE_ATTRIBUTE_NAME) ? toInteger(innerErrorObject.getString(ERROR_CODE_ATTRIBUTE_NAME)) : null; throw graphFacebookExceptionMapper.exceptionForTypeAndMessage(errorCode, httpStatusCode, innerErrorObject.getString(ERROR_TYPE_ATTRIBUTE_NAME), innerErrorObject.getString(ERROR_MESSAGE_ATTRIBUTE_NAME)); } catch (JsonException e) { throw new FacebookJsonMappingException("Unable to process the Facebook API response", e); } } /** * If the {@code error} and {@code error_description} JSON fields are present, we've got a response status error for * this batch API call. Extracts relevant information from the JSON and throws an exception which encapsulates it for * end-user consumption. * * @param json * The JSON returned by Facebook in response to a batch API call. * @param httpStatusCode * The HTTP status code returned by the server, e.g. 500. * @throws FacebookResponseStatusException * If the JSON contains an error code. * @throws FacebookJsonMappingException * If an error occurs while processing the JSON. * @since 1.6.5 */ protected void throwBatchFacebookResponseStatusExceptionIfNecessary(String json, Integer httpStatusCode) { try { // If this is not an object, it's not an error response. if (!json.startsWith("{")) return; JsonObject errorObject = null; // We need to swallow exceptions here because it's possible to get a legit // Facebook response that contains illegal JSON (e.g. // users.getLoggedInUser returning 1240077) - we're only interested in // whether or not there's an error_code field present. try { errorObject = new JsonObject(json); } catch (JsonException e) { } if (errorObject == null || !errorObject.has(BATCH_ERROR_ATTRIBUTE_NAME) || !errorObject.has(BATCH_ERROR_DESCRIPTION_ATTRIBUTE_NAME)) return; throw legacyFacebookExceptionMapper.exceptionForTypeAndMessage( errorObject.getInt(BATCH_ERROR_ATTRIBUTE_NAME), httpStatusCode, null, errorObject.getString(BATCH_ERROR_DESCRIPTION_ATTRIBUTE_NAME)); } catch (JsonException e) { throw new FacebookJsonMappingException("Unable to process the Facebook API response", e); } } /** * Specifies how we map Graph API exception types/messages to real Java exceptions. * <p> * Uses an instance of {@link DefaultGraphFacebookExceptionMapper} by default. * * @return An instance of the exception mapper we should use. * @since 1.6 */ protected FacebookExceptionMapper createGraphFacebookExceptionMapper() { return new DefaultGraphFacebookExceptionMapper(); } /** * A canned implementation of {@code FacebookExceptionMapper} that maps Graph API exceptions. * <p> * Thanks to BatchFB's Jeff Schnitzer for doing some of the legwork to find these exception type names. * * @author <a href="http://restfb.com">Mark Allen</a> * @since 1.6.3 */ protected static class DefaultGraphFacebookExceptionMapper implements FacebookExceptionMapper { /** * @see com.restfb.exception.FacebookExceptionMapper#exceptionForTypeAndMessage(java.lang.Integer, * java.lang.Integer, java.lang.String, java.lang.String) */ public FacebookException exceptionForTypeAndMessage(Integer errorCode, Integer httpStatusCode, String type, String message) { if ("OAuthException".equals(type) || "OAuthAccessTokenException".equals(type)) return new FacebookOAuthException(type, message, errorCode, httpStatusCode); if ("QueryParseException".equals(type)) return new FacebookQueryParseException(type, message, httpStatusCode); // Don't recognize this exception type? Just go with the standard // FacebookGraphException. return new FacebookGraphException(type, message, httpStatusCode); } } /** * Generate the parameter string to be included in the Facebook API request. * * @param parameters * Arbitrary number of extra parameters to include in the request. * @return The parameter string to include in the Facebook API request. * @throws FacebookJsonMappingException * If an error occurs when building the parameter string. */ protected String toParameterString(Parameter... parameters) { if (!isBlank(accessToken)) parameters = parametersWithAdditionalParameter(Parameter.with(ACCESS_TOKEN_PARAM_NAME, accessToken), parameters); parameters = parametersWithAdditionalParameter(Parameter.with(FORMAT_PARAM_NAME, "json"), parameters); StringBuilder parameterStringBuilder = new StringBuilder(); boolean first = true; for (Parameter parameter : parameters) { if (first) first = false; else parameterStringBuilder.append("&"); parameterStringBuilder.append(urlEncode(parameter.name)); parameterStringBuilder.append("="); parameterStringBuilder.append(urlEncodedValueForParameterName(parameter.name, parameter.value)); } return parameterStringBuilder.toString(); } /** * @see com.restfb.BaseFacebookClient#createEndpointForApiCall(java.lang.String,boolean) */ @Override protected String createEndpointForApiCall(String apiCall, boolean hasAttachment) { trimToEmpty(apiCall).toLowerCase(); while (apiCall.startsWith("/")) apiCall = apiCall.substring(1); String baseUrl = getFacebookGraphEndpointUrl(); if (readOnlyApiCalls.contains(apiCall)) baseUrl = getFacebookReadOnlyEndpointUrl(); else if (hasAttachment && apiCall.endsWith("/videos")) baseUrl = getFacebookGraphVideoEndpointUrl(); return format("%s/%s", baseUrl, apiCall); } /** * Returns the base endpoint URL for the Graph API. * * @return The base endpoint URL for the Graph API. */ protected String getFacebookGraphEndpointUrl() { return FACEBOOK_GRAPH_ENDPOINT_URL; } /** * Returns the base endpoint URL for the Graph API's video upload functionality. * * @return The base endpoint URL for the Graph API's video upload functionality. * @since 1.6.5 */ protected String getFacebookGraphVideoEndpointUrl() { return FACEBOOK_GRAPH_VIDEO_ENDPOINT_URL; } /** * @see com.restfb.BaseFacebookClient#getFacebookReadOnlyEndpointUrl() */ @Override protected String getFacebookReadOnlyEndpointUrl() { return FACEBOOK_READ_ONLY_ENDPOINT_URL; } /** * Gets value from from given listenable future asserting that exceptions thrown by * future are limited to FacebookException class. */ private <T> T facebookFutureGet(ListenableFuture<T> future) throws FacebookException { try { return future.get(); } catch (InterruptedException e) { throw new FacebookNetworkException("interrupted", e); } catch (ExecutionException e) { assert e.getCause() instanceof FacebookException; throw (FacebookException) e.getCause(); } } /** * Transforms future to json response body to future with object using {@link JsonMapper#toJavaObject(String, Class)} method */ private <T> ListenableFuture<T> transformJsonToJavaObject(ListenableFuture<String> resultBodyFuture, final Class<T> objectType) { return Futures.transform(resultBodyFuture, new Function<String, T>() { @Override public T apply(@Nullable String input) { return jsonMapper.toJavaObject(input, objectType); } }); } }