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}