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