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