001/* 002 * Copyright (c) 2010-2026 Mark Allen, Norbert Bartels. 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining a copy 005 * of this software and associated documentation files (the "Software"), to deal 006 * in the Software without restriction, including without limitation the rights 007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 008 * copies of the Software, and to permit persons to whom the Software is 009 * furnished to do so, subject to the following conditions: 010 * 011 * The above copyright notice and this permission notice shall be included in 012 * all copies or substantial portions of the Software. 013 * 014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 020 * THE SOFTWARE. 021 */ 022package com.restfb.exception.generator; 023 024import static com.restfb.util.StringUtils.toInteger; 025 026import java.util.Optional; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029 030import com.restfb.exception.*; 031import com.restfb.json.Json; 032import com.restfb.json.JsonObject; 033import com.restfb.json.ParseException; 034 035public class DefaultFacebookExceptionGenerator implements FacebookExceptionGenerator { 036 037 /** 038 * Knows how to map Graph API exceptions to formal Java exception types. 039 */ 040 protected FacebookExceptionMapper graphFacebookExceptionMapper; 041 042 private static final Pattern ERROR_PATTERN = Pattern.compile("\"error[_a-z]*\"\\s*:"); 043 044 public DefaultFacebookExceptionGenerator() { 045 super(); 046 graphFacebookExceptionMapper = createGraphFacebookExceptionMapper(); 047 } 048 049 @Override 050 public void throwFacebookResponseStatusExceptionIfNecessary(String json, Integer httpStatusCode) { 051 try { 052 skipResponseStatusExceptionParsing(json); 053 054 throwLoginOauthExceptionIfNecessary(json, httpStatusCode); 055 056 // If we have a batch API exception, throw it. 057 throwBatchFacebookResponseStatusExceptionIfNecessary(json, httpStatusCode); 058 059 JsonObject errorObject = Json.parse(json).asObject(); 060 061 if (!errorObject.contains(ERROR_ATTRIBUTE_NAME)) { 062 return; 063 } 064 065 ExceptionInformation container = createFacebookResponseTypeAndMessageContainer(errorObject, httpStatusCode); 066 067 throw graphFacebookExceptionMapper.exceptionForTypeAndMessage(container); 068 } catch (ParseException e) { 069 throw new FacebookJsonMappingException("Unable to process the Facebook API response", e); 070 } catch (ResponseErrorJsonParsingException ex) { 071 // do nothing here 072 } 073 } 074 075 private void throwLoginOauthExceptionIfNecessary(String json, Integer httpStatusCode) { 076 JsonObject errorObject = silentlyCreateObjectFromString(json); 077 078 if (errorObject == null || errorObject.contains(BATCH_ERROR_ATTRIBUTE_NAME)) { 079 return; 080 } 081 082 errorObject = getErrorObjectIfData(errorObject); 083 084 String errorType = errorObject.getString("error_type", null); 085 Integer errorCode = errorObject.getInt("code", 0); 086 String errorMessage = errorObject.getString("error_message", null); 087 if (errorMessage == null && errorObject.contains("message")) { 088 errorMessage = errorObject.getString("message", null); 089 } 090 091 ExceptionInformation container = new ExceptionInformation(errorCode, null, httpStatusCode, errorType, errorMessage, 092 null, null, false, errorObject); 093 throw graphFacebookExceptionMapper.exceptionForTypeAndMessage(container); 094 } 095 096 private JsonObject getErrorObjectIfData(JsonObject errorObject) { 097 if (errorObject.contains("data") && errorObject.get("data").isObject()) { 098 JsonObject data = errorObject.get("data").asObject(); 099 if (data.contains("error") && data.get("error").isObject()) { 100 return data.get("error").asObject(); 101 } 102 } 103 104 return errorObject; 105 } 106 107 protected ExceptionInformation createFacebookResponseTypeAndMessageContainer(JsonObject errorObject, 108 Integer httpStatusCode) { 109 JsonObject innerErrorObject = errorObject.get(ERROR_ATTRIBUTE_NAME).asObject(); 110 111 // If there's an Integer error code, pluck it out. 112 Integer errorCode = Optional.ofNullable(innerErrorObject.get(ERROR_CODE_ATTRIBUTE_NAME)) 113 .map(obj -> toInteger(obj.toString())).orElse(null); 114 Integer errorSubcode = Optional.ofNullable(innerErrorObject.get(ERROR_SUBCODE_ATTRIBUTE_NAME)) 115 .map(obj -> toInteger(obj.toString())).orElse(null); 116 117 return new ExceptionInformation(errorCode, errorSubcode, httpStatusCode, 118 innerErrorObject.getString(ERROR_TYPE_ATTRIBUTE_NAME, null), 119 innerErrorObject.get(ERROR_MESSAGE_ATTRIBUTE_NAME).asString(), 120 innerErrorObject.getString(ERROR_USER_TITLE_ATTRIBUTE_NAME, null), 121 innerErrorObject.getString(ERROR_USER_MSG_ATTRIBUTE_NAME, null), 122 innerErrorObject.getBoolean(ERROR_IS_TRANSIENT_NAME, false), errorObject); 123 } 124 125 @Override 126 public void throwBatchFacebookResponseStatusExceptionIfNecessary(String json, Integer httpStatusCode) { 127 try { 128 skipResponseStatusExceptionParsing(json); 129 130 JsonObject errorObject = silentlyCreateObjectFromString(json); 131 132 if (errorObject == null || errorObject.contains(BATCH_ERROR_ATTRIBUTE_NAME) 133 || errorObject.contains(BATCH_ERROR_DESCRIPTION_ATTRIBUTE_NAME) 134 // not a batch response, if data key is present 135 || errorObject.contains("data")) 136 return; 137 138 ExceptionInformation container = new ExceptionInformation(errorObject.getInt(BATCH_ERROR_ATTRIBUTE_NAME, 0), 139 httpStatusCode, errorObject.getString(BATCH_ERROR_DESCRIPTION_ATTRIBUTE_NAME, null), errorObject); 140 141 throw graphFacebookExceptionMapper.exceptionForTypeAndMessage(container); 142 } catch (ParseException e) { 143 throw new FacebookJsonMappingException("Unable to process the Facebook API response", e); 144 } catch (ResponseErrorJsonParsingException ex) { 145 // do nothing here 146 } 147 } 148 149 /** 150 * Specifies how we map Graph API exception types/messages to real Java exceptions. 151 * <p> 152 * Uses an instance of {@link DefaultGraphFacebookExceptionMapper} by default. 153 * 154 * @return An instance of the exception mapper we should use. 155 * @since 1.6 156 */ 157 protected FacebookExceptionMapper createGraphFacebookExceptionMapper() { 158 return new DefaultGraphFacebookExceptionMapper(); 159 } 160 161 /** 162 * checks if a string may be a json and contains a error string somewhere, this is used for speedup the error parsing 163 * 164 * @param json 165 */ 166 protected void skipResponseStatusExceptionParsing(String json) throws ResponseErrorJsonParsingException { 167 // If this is not an object, it's not an error response. 168 if (!json.startsWith("{")) { 169 throw new ResponseErrorJsonParsingException(); 170 } 171 172 int subStrEnd = Math.min(50, json.length()); 173 Matcher matcher = ERROR_PATTERN.matcher(json.substring(0, subStrEnd)); 174 if (!matcher.find()) { 175 throw new ResponseErrorJsonParsingException(); 176 } 177 } 178 179 /** 180 * create a {@link JsonObject} from String and swallow possible JsonException 181 * 182 * @param json 183 * the string representation of the json 184 * @return the JsonObject, may be <code>null</code> 185 */ 186 protected JsonObject silentlyCreateObjectFromString(String json) { 187 JsonObject errorObject = null; 188 189 // We need to swallow exceptions here because it's possible to get a legit 190 // Facebook response that contains illegal JSON (e.g. 191 // users.getLoggedInUser returning 1240077) - we're only interested in 192 // whether or not there's an error_code field present. 193 try { 194 errorObject = Json.parse(json).asObject(); 195 } catch (ParseException e) { 196 // do nothing here 197 } 198 199 return errorObject; 200 } 201 202 /** 203 * A canned implementation of {@link FacebookExceptionMapper} that maps Graph API exceptions. 204 * <p> 205 * Thanks to BatchFB's Jeff Schnitzer for doing some of the legwork to find these exception type names. 206 * 207 * @author <a href="http://restfb.com">Mark Allen</a> 208 * @since 1.6.3 209 */ 210 protected static class DefaultGraphFacebookExceptionMapper implements FacebookExceptionMapper { 211 212 @Override 213 public FacebookException exceptionForTypeAndMessage(ExceptionInformation container) { 214 if ("OAuthException".equals(container.getType()) || "OAuthAccessTokenException".equals(container.getType())) { 215 return new FacebookOAuthException(container.getType(), container.getMessage(), container.getErrorCode(), 216 container.getErrorSubcode(), container.getHttpStatusCode(), container.getUserTitle(), 217 container.getUserMessage(), container.getIsTransient(), container.getRawError()); 218 } 219 220 if ("QueryParseException".equals(container.getType())) { 221 return new FacebookQueryParseException(container.getType(), container.getMessage(), container.getErrorCode(), 222 container.getErrorSubcode(), container.getHttpStatusCode(), container.getUserTitle(), 223 container.getUserMessage(), container.getIsTransient(), container.getRawError()); 224 } 225 226 if ("THApiException".equals(container.getType())) { 227 return new ThreadsApiException(container.getType(), container.getMessage(), container.getErrorCode(), 228 container.getErrorSubcode(), container.getHttpStatusCode(), container.getIsTransient(), 229 container.getRawError()); 230 } 231 232 if ("IGApiException".equals(container.getType())) { 233 return new InstagramApiException(container.getType(), container.getMessage(), container.getErrorCode(), 234 container.getErrorSubcode(), container.getHttpStatusCode(), container.getIsTransient(), 235 container.getRawError()); 236 } 237 238 // Don't recognize this exception type? Just go with the standard 239 // FacebookGraphException. 240 return new FacebookGraphException(container.getType(), container.getMessage(), container.getErrorCode(), 241 container.getErrorSubcode(), container.getHttpStatusCode(), container.getUserTitle(), 242 container.getUserMessage(), container.getIsTransient(), container.getRawError()); 243 } 244 } 245}