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}