001/* 002 * Copyright (c) 2010-2024 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 throwLoginOauthExceprtionIfNecessary(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 throwLoginOauthExceprtionIfNecessary(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 String errorType = errorObject.getString("error_type", null); 083 Integer errorCode = errorObject.getInt("code", 0); 084 String errorMessage = errorObject.getString("error_message", null); 085 086 ExceptionInformation container = new ExceptionInformation(errorCode, null, httpStatusCode, errorType, errorMessage, 087 null, null, false, errorObject); 088 throw graphFacebookExceptionMapper.exceptionForTypeAndMessage(container); 089 } 090 091 protected ExceptionInformation createFacebookResponseTypeAndMessageContainer(JsonObject errorObject, 092 Integer httpStatusCode) { 093 JsonObject innerErrorObject = errorObject.get(ERROR_ATTRIBUTE_NAME).asObject(); 094 095 // If there's an Integer error code, pluck it out. 096 Integer errorCode = Optional.ofNullable(innerErrorObject.get(ERROR_CODE_ATTRIBUTE_NAME)) 097 .map(obj -> toInteger(obj.toString())).orElse(null); 098 Integer errorSubcode = Optional.ofNullable(innerErrorObject.get(ERROR_SUBCODE_ATTRIBUTE_NAME)) 099 .map(obj -> toInteger(obj.toString())).orElse(null); 100 101 return new ExceptionInformation(errorCode, errorSubcode, httpStatusCode, 102 innerErrorObject.getString(ERROR_TYPE_ATTRIBUTE_NAME, null), 103 innerErrorObject.get(ERROR_MESSAGE_ATTRIBUTE_NAME).asString(), 104 innerErrorObject.getString(ERROR_USER_TITLE_ATTRIBUTE_NAME, null), 105 innerErrorObject.getString(ERROR_USER_MSG_ATTRIBUTE_NAME, null), 106 innerErrorObject.getBoolean(ERROR_IS_TRANSIENT_NAME, false), errorObject); 107 } 108 109 @Override 110 public void throwBatchFacebookResponseStatusExceptionIfNecessary(String json, Integer httpStatusCode) { 111 try { 112 skipResponseStatusExceptionParsing(json); 113 114 JsonObject errorObject = silentlyCreateObjectFromString(json); 115 116 if (errorObject == null || errorObject.contains(BATCH_ERROR_ATTRIBUTE_NAME) 117 || errorObject.contains(BATCH_ERROR_DESCRIPTION_ATTRIBUTE_NAME) 118 // not a batch response, if data key is present 119 || errorObject.contains("data")) 120 return; 121 122 ExceptionInformation container = new ExceptionInformation(errorObject.getInt(BATCH_ERROR_ATTRIBUTE_NAME, 0), 123 httpStatusCode, errorObject.getString(BATCH_ERROR_DESCRIPTION_ATTRIBUTE_NAME, null), errorObject); 124 125 throw graphFacebookExceptionMapper.exceptionForTypeAndMessage(container); 126 } catch (ParseException e) { 127 throw new FacebookJsonMappingException("Unable to process the Facebook API response", e); 128 } catch (ResponseErrorJsonParsingException ex) { 129 // do nothing here 130 } 131 } 132 133 /** 134 * Specifies how we map Graph API exception types/messages to real Java exceptions. 135 * <p> 136 * Uses an instance of {@link DefaultGraphFacebookExceptionMapper} by default. 137 * 138 * @return An instance of the exception mapper we should use. 139 * @since 1.6 140 */ 141 protected FacebookExceptionMapper createGraphFacebookExceptionMapper() { 142 return new DefaultGraphFacebookExceptionMapper(); 143 } 144 145 /** 146 * checks if a string may be a json and contains a error string somewhere, this is used for speedup the error parsing 147 * 148 * @param json 149 */ 150 protected void skipResponseStatusExceptionParsing(String json) throws ResponseErrorJsonParsingException { 151 // If this is not an object, it's not an error response. 152 if (!json.startsWith("{")) { 153 throw new ResponseErrorJsonParsingException(); 154 } 155 156 int subStrEnd = Math.min(50, json.length()); 157 Matcher matcher = ERROR_PATTERN.matcher(json.substring(0, subStrEnd)); 158 if (!matcher.find()) { 159 throw new ResponseErrorJsonParsingException(); 160 } 161 } 162 163 /** 164 * create a {@link JsonObject} from String and swallow possible JsonException 165 * 166 * @param json 167 * the string representation of the json 168 * @return the JsonObject, may be <code>null</code> 169 */ 170 protected JsonObject silentlyCreateObjectFromString(String json) { 171 JsonObject errorObject = null; 172 173 // We need to swallow exceptions here because it's possible to get a legit 174 // Facebook response that contains illegal JSON (e.g. 175 // users.getLoggedInUser returning 1240077) - we're only interested in 176 // whether or not there's an error_code field present. 177 try { 178 errorObject = Json.parse(json).asObject(); 179 } catch (ParseException e) { 180 // do nothing here 181 } 182 183 return errorObject; 184 } 185 186 /** 187 * A canned implementation of {@link FacebookExceptionMapper} that maps Graph API exceptions. 188 * <p> 189 * Thanks to BatchFB's Jeff Schnitzer for doing some of the legwork to find these exception type names. 190 * 191 * @author <a href="http://restfb.com">Mark Allen</a> 192 * @since 1.6.3 193 */ 194 protected static class DefaultGraphFacebookExceptionMapper implements FacebookExceptionMapper { 195 196 @Override 197 public FacebookException exceptionForTypeAndMessage(ExceptionInformation container) { 198 if ("OAuthException".equals(container.getType()) || "OAuthAccessTokenException".equals(container.getType())) { 199 return new FacebookOAuthException(container.getType(), container.getMessage(), container.getErrorCode(), 200 container.getErrorSubcode(), container.getHttpStatusCode(), container.getUserTitle(), 201 container.getUserMessage(), container.getIsTransient(), container.getRawError()); 202 } 203 204 if ("QueryParseException".equals(container.getType())) { 205 return new FacebookQueryParseException(container.getType(), container.getMessage(), container.getErrorCode(), 206 container.getErrorSubcode(), container.getHttpStatusCode(), container.getUserTitle(), 207 container.getUserMessage(), container.getIsTransient(), container.getRawError()); 208 } 209 210 if ("THApiException".equals(container.getType())) { 211 return new ThreadsApiException(container.getType(), container.getMessage(), container.getErrorCode(), 212 container.getErrorSubcode(), container.getHttpStatusCode(), container.getIsTransient(), 213 container.getRawError()); 214 } 215 216 // Don't recognize this exception type? Just go with the standard 217 // FacebookGraphException. 218 return new FacebookGraphException(container.getType(), container.getMessage(), container.getErrorCode(), 219 container.getErrorSubcode(), container.getHttpStatusCode(), container.getUserTitle(), 220 container.getUserMessage(), container.getIsTransient(), container.getRawError()); 221 } 222 } 223}