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}