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