com.googlecode.batchfb.impl.ErrorDetectingWrapper.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.batchfb.impl.ErrorDetectingWrapper.java

Source

/*
 * Copyright (c) 2010 Jeff Schnitzer.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.googlecode.batchfb.impl;

import com.fasterxml.jackson.databind.JsonNode;
import com.googlecode.batchfb.Batcher;
import com.googlecode.batchfb.Later;
import com.googlecode.batchfb.err.ErrorFacebookException;
import com.googlecode.batchfb.err.FacebookException;
import com.googlecode.batchfb.err.OAuthException;
import com.googlecode.batchfb.err.PageMigratedException;
import com.googlecode.batchfb.err.PermissionException;
import com.googlecode.batchfb.util.LaterWrapper;

import java.lang.reflect.Constructor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>Detects a Facebook error in the JSON result from a Graph API request and throws
 * the correct kind of exception, whatever that happens to be.  It tries to match the
 * type of the exception with an actual exception class of the correct name.</p>
 * 
 * <p>In addition, this detects the REALLY WEIRD cases where Facebook just spits back
 * "false".  We translate that into a null.</p>
 * 
 * <p>If there was no error, this wrapper just passes through normally.</p>
 * 
 * <p>Facebook coughs up errors in at least three different formats.  This
 * detects them all.</p>
 */
public class ErrorDetectingWrapper extends LaterWrapper<JsonNode, JsonNode> {
    public ErrorDetectingWrapper(Later<JsonNode> orig) {
        super(orig);
    }

    /** */
    @Override
    protected JsonNode convert(JsonNode node) {
        // Hopefully a simple "false" at the top level is never a legitimate value... it seems that it should be mapped
        // to null.  It happens (among other times) when fetching multiple items and you don't have permission on one of them.
        if (node == null || node.isBoolean() && !node.booleanValue())
            return null;

        this.checkForStandardGraphError(node);
        this.checkForBatchError(node);
        this.checkForOldRestStyleError(node);

        return node;
    }

    /**
     * The basic graph error looks like this:
    <pre>
    {
    error: {
     type: "OAuthException"
     message: "Error validating application."
    }
    }
    </pre>
     */
    protected void checkForStandardGraphError(JsonNode node) {
        JsonNode errorNode = node.get("error");
        if (errorNode != null) {
            // If we're missing type or message, it must be some other kind of error
            String type = errorNode.path("type").textValue();
            if (type == null)
                return;

            String msg = errorNode.path("message").textValue();
            if (msg == null)
                return;

            JsonNode codeNode = errorNode.get("code");
            Integer code = codeNode == null ? null : codeNode.intValue();

            JsonNode subcodeNode = errorNode.get("error_subcode");
            Integer subcode = subcodeNode == null ? null : subcodeNode.intValue();

            String userTitle = errorNode.path("error_user_title").textValue();
            String userMsg = errorNode.path("error_user_msg").textValue();

            if (code != null) {
                // Special case, migration exceptions are poorly structured
                if (code == 21)
                    this.throwPageMigratedException(msg, code, subcode, userTitle, userMsg);

                // Documented here: https://developers.facebook.com/docs/graph-api/using-graph-api
                if (code == 10 || (code >= 200 && code <= 299))
                    throw new PermissionException(msg, type, code, subcode, userTitle, userMsg);
            }

            // We check to see if we have an exception that matches the type, otherwise
            // we simply throw the base FacebookException
            String proposedExceptionType = Batcher.class.getPackage().getName() + ".err." + type;

            try {
                Class<?> exceptionClass = Class.forName(proposedExceptionType);
                Constructor<?> ctor = exceptionClass.getConstructor(String.class, String.class, Integer.TYPE,
                        Integer.TYPE);
                throw (FacebookException) ctor.newInstance(msg, type, code, subcode);
            } catch (FacebookException e) {
                throw e;
            } catch (Exception e) {
                throw new ErrorFacebookException(type + ": " + msg, type, code, subcode, userTitle, userMsg);
            }
        }
    }

    /** Matches IDs in the error msg */
    private static final Pattern ID_PATTERN = Pattern.compile("ID [0-9]+");

    /**
     * Builds the proper exception and throws it.
     * @throws PageMigratedException always
     */
    private void throwPageMigratedException(String msg, int code, int subcode, String userTitle, String userMsg) {
        // This SUCKS ASS.  Messages look like:
        // (#21) Page ID 114267748588304 was migrated to page ID 111013272313096.  Please update your API calls to the new ID

        Matcher matcher = ID_PATTERN.matcher(msg);

        long oldId = this.extractNextId(matcher, msg);
        long newId = this.extractNextId(matcher, msg);

        throw new PageMigratedException(msg, code, subcode, userTitle, userMsg, oldId, newId);
    }

    /**
     * Gets the next id out of the matcher
     */
    private long extractNextId(Matcher matcher, String msg) {
        if (!matcher.find())
            throw new IllegalStateException(
                    "Facebook changed the error msg for page migration to something unfamiliar. The new msg is: "
                            + msg);

        String idStr = matcher.group().substring("ID ".length());
        return Long.parseLong(idStr);
    }

    /**
     * The batch call itself seems to have a funky error format:
     * 
     * {"error":190,"error_description":"Invalid OAuth access token signature."}
     */
    protected void checkForBatchError(JsonNode root) {
        JsonNode errorCode = root.get("error");
        if (errorCode != null) {

            JsonNode errorDescription = root.get("error_description");
            if (errorDescription != null) {

                int code = errorCode.intValue();
                String msg = errorDescription.asText();

                this.throwCodeAndMessage(code, msg);
            }
        }
    }

    /**
     * Old-style calls, including multiquery, has its own wacky error format:
    <pre>
    {
    "error_code": 602,
    "error_msg": "bogus is not a member of the user table.",
    "request_args": [
     {
       "key": "queries",
       "value": "{"query1":"SELECT uid FROM user WHERE uid=503702723",
    "query2":"SELECT uid FROM user WHERE bogus=503702723"}"
     },
     {
       "key": "method",
       "value": "fql.multiquery"
     },
     {
       "key": "access_token",
       "value": "blahblahblah"
     },
     {
       "key": "format",
       "value": "json"
     }
    ]
    }
    </pre>
     *
     * The code interpretations rely heavily on http://wiki.developers.facebook.com/index.php/Error_codes
     * The wayback machine: https://web.archive.org/web/20091223080550/http://wiki.developers.facebook.com/index.php/Error_codes
     */
    protected void checkForOldRestStyleError(JsonNode node) {
        JsonNode errorCode = node.get("error_code");

        if (errorCode != null) {
            int code = errorCode.intValue();
            String msg = node.path("error_msg").asText();

            this.throwCodeAndMessage(code, msg);
        }
    }

    /**
     * Throw the appropriate exception for the given legacy code and message.
     * Always throws, never returns.
     */
    protected void throwCodeAndMessage(int code, String msg) {
        switch (code) {
        case 0:
        case 101:
        case 102:
        case 190:
            throw new OAuthException(msg, "OAuthException", code, null, null, null);

        default:
            throw new ErrorFacebookException(msg + " (code " + code + ")", null, code, null, null, null);
        }
    }
}