FileDocCategorySizeDatePackage
FacebookRestClient.javaAPI DocGoogle Facebook API v1.4105711Fri Nov 02 16:41:16 GMT 2007com.facebook.api

FacebookRestClient.java

/*
  +---------------------------------------------------------------------------+
  | Facebook Development Platform Java Client                                 |
  +---------------------------------------------------------------------------+
  | Copyright (c) 2007 Facebook, Inc.                                         |
  | All rights reserved.                                                      |
  |                                                                           |
  | Redistribution and use in source and binary forms, with or without        |
  | modification, are permitted provided that the following conditions        |
  | are met:                                                                  |
  |                                                                           |
  | 1. Redistributions of source code must retain the above copyright         |
  |    notice, this list of conditions and the following disclaimer.          |
  | 2. Redistributions in binary form must reproduce the above copyright      |
  |    notice, this list of conditions and the following disclaimer in the    |
  |    documentation and/or other materials provided with the distribution.   |
  |                                                                           |
  | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR      |
  | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
  | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.   |
  | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,          |
  | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT  |
  | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
  | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY     |
  | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT       |
  | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF  |
  | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.         |
  +---------------------------------------------------------------------------+
  | For help with this library, contact developers-help@facebook.com          |
  +---------------------------------------------------------------------------+
 */

package com.facebook.api;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.facebook.api.schema.Listing;
import com.facebook.api.schema.MarketplaceGetCategoriesResponse;
import com.facebook.api.schema.MarketplaceGetListingsResponse;
import com.facebook.api.schema.MarketplaceGetSubCategoriesResponse;
import com.facebook.api.schema.MarketplaceSearchResponse;

/**
 * Facebook API client.  Allocate an instance of this class to make Facebook API requests.
 */
public class FacebookRestClient implements IFacebookRestClient<Document>{
  /**
   * API version to request when making calls to the server
   */
  public static final String TARGET_API_VERSION = "1.0";
  /**
   * Flag indicating an erroneous response
   */
  public static final String ERROR_TAG = "error_response";
  /**
   * Facebook API server, part 1
   */
  public static final String FB_SERVER = "api.facebook.com/restserver.php";
  /**
   * Facebook API server, part 2a
   */
  public static final String SERVER_ADDR = "http://" + FB_SERVER;
  /**
   * Facebook API server, part 2b
   */
  public static final String HTTPS_SERVER_ADDR = "https://" + FB_SERVER;
  /**
   * Facebook API server, part 3a
   */
  public static URL SERVER_URL = null;
  /**
   * Facebook API server, part 3b
   */
  public static URL HTTPS_SERVER_URL = null;
  static {
    try {
      SERVER_URL = new URL(SERVER_ADDR);
      HTTPS_SERVER_URL = new URL(HTTPS_SERVER_ADDR);
    }
    catch (MalformedURLException e) {
      System.err.println("MalformedURLException: " + e.getMessage());
      System.exit(1);
    }
  }

  protected final String _secret;
  protected final String _apiKey;
  protected final URL _serverUrl;
  protected String rawResponse;

  protected String _sessionKey; // filled in when session is established
  protected boolean _isDesktop = false;
  protected String _sessionSecret; // only used for desktop apps
  protected long _userId;

  /**
   * number of params that the client automatically appends to every API call
   */
  public static int NUM_AUTOAPPENDED_PARAMS = 6;
  private static boolean DEBUG = false;
  protected Boolean _debug = null;

  protected File _uploadFile = null;
  protected static final String CRLF = "\r\n";
  protected static final String PREF = "--";
  protected static final int UPLOAD_BUFFER_SIZE = 512;

  public static final String MARKETPLACE_STATUS_DEFAULT     = "DEFAULT";
  public static final String MARKETPLACE_STATUS_NOT_SUCCESS = "NOT_SUCCESS";
  public static final String MARKETPLACE_STATUS_SUCCESS     = "SUCCESS";

  /**
   * Constructor
   *
   * @param apiKey the developer's API key
   * @param secret the developer's secret key
   */
  public FacebookRestClient(String apiKey, String secret) {
    this(SERVER_URL, apiKey, secret, null);
  }

  /**
   * Constructor
   *
   * @param apiKey the developer's API key
   * @param secret the developer's secret key
   * @param sessionKey the session-id to use
   */
  public FacebookRestClient(String apiKey, String secret, String sessionKey) {
    this(SERVER_URL, apiKey, secret, sessionKey);
  }

  /**
   * Constructor
   *
   * @param serverAddr the URL of the Facebook API server to use, allows overriding of the default API server.
   * @param apiKey the developer's API key
   * @param secret the developer's secret key
   * @param sessionKey the session-id to use
   *
   * @throws MalformedURLException if the specified serverAddr is invalid
   */
  public FacebookRestClient(String serverAddr, String apiKey, String secret,
                            String sessionKey) throws MalformedURLException {
    this(new URL(serverAddr), apiKey, secret, sessionKey);
  }

  /**
   * Constructor
   *
   * @param serverUrl the URL of the Facebook API server to use, allows overriding of the default API server.
   * @param apiKey the developer's API key
   * @param secret the developer's secret key
   * @param sessionKey the session-id to use
   */
  public FacebookRestClient(URL serverUrl, String apiKey, String secret, String sessionKey) {
    _sessionKey = sessionKey;
    _apiKey = apiKey;
    _secret = secret;
    _serverUrl = (null != serverUrl) ? serverUrl : SERVER_URL;
  }

  /**
   * The response format in which results to FacebookMethod calls are returned
   * @return the format: either XML, JSON, or null (API default)
   */
  public String getResponseFormat() {
      return "xml";
  }

  /**
   * Set global debugging on.
   *
   * @param isDebug true to enable debugging
   *                false to disable debugging
   */
  public static void setDebugAll(boolean isDebug) {
    FacebookRestClient.DEBUG = isDebug;
  }

  /**
   * Set debugging on for this instance only.
   *
   * @param isDebug true to enable debugging
   *                false to disable debugging
   */
  //FIXME:  do we really need both of these?
  public void setDebug(boolean isDebug) {
    _debug = isDebug;
  }

  /**
   * Check to see if debug mode is enabled.
   *
   * @return true if debugging is enabled
   *         false otherwise
   */
  public boolean isDebug() {
    return (null == _debug) ? FacebookRestClient.DEBUG : _debug.booleanValue();
  }

  /**
   * Check to see if the client is running in desktop mode.
   *
   * @return true if the client is running in desktop mode
   *         false otherwise
   */
  public boolean isDesktop() {
    return this._isDesktop;
  }

  /**
   * Enable/disable desktop mode.
   *
   * @param isDesktop true to enable desktop application mode
   *                  false to disable desktop application mode
   */
  public void setIsDesktop(boolean isDesktop) {
    this._isDesktop = isDesktop;
  }

  /**
   * Prints out the DOM tree.
   *
   * @param n the parent node to start printing from
   * @param prefix string to append to output, should not be null
   */
  public static void printDom(Node n, String prefix) {
    String outString = prefix;
    if (n.getNodeType() == Node.TEXT_NODE) {
      outString += "'" + n.getTextContent().trim() + "'";
    }
    else {
      outString += n.getNodeName();
    }
    if (DEBUG) {
        System.out.println(outString);
    }
    NodeList children = n.getChildNodes();
    int length = children.getLength();
    for (int i = 0; i < length; i++) {
      FacebookRestClient.printDom(children.item(i), prefix + "  ");
    }
  }

  private static CharSequence delimit(Collection<?> iterable) {
    // could add a thread-safe version that uses StringBuffer as well
    if (iterable == null || iterable.isEmpty())
      return null;

    StringBuilder buffer = new StringBuilder();
    boolean notFirst = false;
    for (Object item: iterable) {
      if (notFirst)
        buffer.append(",");
      else
        notFirst = true;
      buffer.append(item.toString());
    }
    return buffer;
  }

  protected static CharSequence delimit(Collection<Map.Entry<String, CharSequence>> entries,
                                        CharSequence delimiter, CharSequence equals,
                                        boolean doEncode) {
    if (entries == null || entries.isEmpty())
      return null;

    StringBuilder buffer = new StringBuilder();
    boolean notFirst = false;
    for (Map.Entry<String, CharSequence> entry: entries) {
      if (notFirst)
        buffer.append(delimiter);
      else
        notFirst = true;
      CharSequence value = entry.getValue();
      buffer.append(entry.getKey()).append(equals).append(doEncode ? encode(value) : value);
    }
    return buffer;
  }

  /**
   * Call the specified method, with the given parameters, and return a DOM tree with the results.
   *
   * @param method the fieldName of the method
   * @param paramPairs a list of arguments to the method
   * @throws Exception with a description of any errors given to us by the server.
   */
  protected Document callMethod(FacebookMethod method,
                                Pair<String, CharSequence>... paramPairs) throws FacebookException,
                                                                                 IOException {
    return callMethod(method, Arrays.asList(paramPairs));
  }

  /**
   * Call the specified method, with the given parameters, and return a DOM tree with the results.
   *
   * @param method the fieldName of the method
   * @param paramPairs a list of arguments to the method
   * @throws Exception with a description of any errors given to us by the server.
   */
  protected Document callMethod(FacebookMethod method,
                                Collection<Pair<String, CharSequence>> paramPairs) throws FacebookException,
                                                                                          IOException {
    this.rawResponse = null;
    HashMap<String, CharSequence> params =
      new HashMap<String, CharSequence>(2 * method.numTotalParams());

    params.put("method", method.methodName());
    params.put("api_key", _apiKey);
    params.put("v", TARGET_API_VERSION);
    if (method.requiresSession()) {
      params.put("call_id", Long.toString(System.currentTimeMillis()));
      params.put("session_key", _sessionKey);
    }
    CharSequence oldVal;
    for (Pair<String, CharSequence> p: paramPairs) {
      oldVal = params.put(p.first, p.second);
      if (oldVal != null)
          System.out.println("For parameter " + p.first + ", overwrote old value " + oldVal +
                " with new value " + p.second + ".");
    }

    assert (!params.containsKey("sig"));
    String signature = generateSignature(FacebookSignatureUtil.convert(params.entrySet()), method.requiresSession());
    params.put("sig", signature);

    try {
      DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
      boolean doHttps = this.isDesktop() && FacebookMethod.AUTH_GET_SESSION.equals(method);
      InputStream data =
        method.takesFile() ? postFileRequest(method.methodName(), params) : postRequest(method.methodName(),
                                                                                        params,
                                                                                        doHttps,
                                                                                        true);
      /*int current = 0;
      StringBuffer buffer = new StringBuffer();
      while (current != -1) {
          current = data.read();
          if (current != -1) {
              buffer.append((char)current);
          }
      }*/

      BufferedReader in = new BufferedReader(new InputStreamReader(data, "UTF-8"));
      StringBuffer buffer = new StringBuffer();
      String line;
      while ((line = in.readLine()) != null) {
        buffer.append(line);
      }


      String xmlResp = new String(buffer);
      this.rawResponse = xmlResp;

      Document doc = builder.parse(new ByteArrayInputStream(xmlResp.getBytes("UTF-8")));
      doc.normalizeDocument();
      stripEmptyTextNodes(doc);

      if (isDebug())
        FacebookRestClient.printDom(doc, method.methodName() + "| "); // TEST
      NodeList errors = doc.getElementsByTagName(ERROR_TAG);
      if (errors.getLength() > 0) {
        int errorCode =
          Integer.parseInt(errors.item(0).getFirstChild().getFirstChild().getTextContent());
        String message = errors.item(0).getFirstChild().getNextSibling().getTextContent();
        // FIXME: additional printing done for debugging only
        System.out.println("Facebook returns error code " + errorCode);
        for (Map.Entry<String,CharSequence> entry : params.entrySet())
            System.out.println("  - " + entry.getKey() + " -> " + entry.getValue());
        throw new FacebookException(errorCode, message);
      }
      return doc;
    }
    catch (java.net.SocketException ex) {
        System.err.println("Socket exception when calling facebook method: " + ex.getMessage());
    }
    catch (javax.xml.parsers.ParserConfigurationException ex) {
        System.err.println("huh?");
        ex.printStackTrace();
    }
    catch (org.xml.sax.SAXException ex) {
      throw new IOException("error parsing xml");
    }
    return null;
  }

  /**
   * Returns a string representation for the last API response recieved from Facebook, exactly as sent by the API server.
   *
   * Note that calling this method consumes the data held in the internal buffer, and thus it may only be called once per API
   * call.
   *
   * @return a String representation of the last API response sent by Facebook
   */
  public String getRawResponse() {
      String result = this.rawResponse;
      this.rawResponse = null;
      return result;
  }

  /**
   * Hack...since DOM reads newlines as textnodes we want to strip out those
   * nodes to make it easier to use the tree.
   */
  private static void stripEmptyTextNodes(Node n) {
    NodeList children = n.getChildNodes();
    int length = children.getLength();
    for (int i = 0; i < length; i++) {
      Node c = children.item(i);
      if (!c.hasChildNodes() && c.getNodeType() == Node.TEXT_NODE &&
          c.getTextContent().trim().length() == 0) {
        n.removeChild(c);
        i--;
        length--;
        children = n.getChildNodes();
      }
      else {
        stripEmptyTextNodes(c);
      }
    }
  }

  private String generateSignature(List<String> params, boolean requiresSession) {
    String secret = (isDesktop() && requiresSession) ? this._sessionSecret : this._secret;
    return FacebookSignatureUtil.generateSignature(params, secret);
  }

  private static String encode(CharSequence target) {
    String result = (target != null) ? target.toString() : "";
    try {
      result = URLEncoder.encode(result, "UTF8");
    }
    catch (UnsupportedEncodingException e) {
        System.err.println("Unsuccessful attempt to encode '" + result + "' into UTF8");
    }
    return result;
  }

  private InputStream postRequest(CharSequence method, Map<String, CharSequence> params,
                                  boolean doHttps, boolean doEncode) throws IOException {
    CharSequence buffer = (null == params) ? "" : delimit(params.entrySet(), "&", "=", doEncode);
    URL serverUrl = (doHttps) ? HTTPS_SERVER_URL : _serverUrl;
    if (isDebug() && DEBUG) {
        System.out.println(method);
        System.out.println(" POST: ");
        System.out.println(serverUrl.toString());
        System.out.println("/");
        System.out.println(buffer);
    }

    HttpURLConnection conn = (HttpURLConnection) serverUrl.openConnection();
    try {
      conn.setRequestMethod("POST");
    }
    catch (ProtocolException ex) {
        System.err.println("huh?");
        ex.printStackTrace();
    }
    conn.setDoOutput(true);
    conn.connect();
    conn.getOutputStream().write(buffer.toString().getBytes());

    return conn.getInputStream();
  }

  /**
   * Sets the FBML for a user's profile, including the content for both the profile box
   * and the profile actions.
   * @param userId - the user whose profile FBML to set
   * @param fbmlMarkup - refer to the FBML documentation for a description of the markup and its role in various contexts
   * @return a boolean indicating whether the FBML was successfully set
   */
  public boolean profile_setFBML(CharSequence fbmlMarkup, Long userId) throws FacebookException, IOException {

    return extractBoolean(this.callMethod(FacebookMethod.PROFILE_SET_FBML,
                          new Pair<String, CharSequence>("uid", Long.toString(userId)),
                          new Pair<String, CharSequence>("markup", fbmlMarkup)));

  }

  /**
   * Gets the FBML for a user's profile, including the content for both the profile box
   * and the profile actions.
   * @param userId - the user whose profile FBML to set
   * @return a Document containing FBML markup
   */
  public Document profile_getFBML(Long userId) throws FacebookException, IOException {
    return this.callMethod(FacebookMethod.PROFILE_GET_FBML,
                          new Pair<String, CharSequence>("uid", Long.toString(userId)));

  }

  /**
   * Recaches the referenced url.
   * @param url string representing the URL to refresh
   * @return boolean indicating whether the refresh succeeded
   */
  public boolean fbml_refreshRefUrl(String url) throws FacebookException, IOException {
    return fbml_refreshRefUrl(new URL(url));
  }

  /**
   * Recaches the referenced url.
   * @param url the URL to refresh
   * @return boolean indicating whether the refresh succeeded
   */
  public boolean fbml_refreshRefUrl(URL url) throws FacebookException, IOException {
    return extractBoolean(this.callMethod(FacebookMethod.FBML_REFRESH_REF_URL,
                                          new Pair<String, CharSequence>("url", url.toString())));
  }

  /**
   * Recaches the image with the specified imageUrl.
   * @param imageUrl String representing the image URL to refresh
   * @return boolean indicating whether the refresh succeeded
   */
  public boolean fbml_refreshImgSrc(String imageUrl) throws FacebookException, IOException {
    return fbml_refreshImgSrc(new URL(imageUrl));
  }

  /**
   * Recaches the image with the specified imageUrl.
   * @param imageUrl the image URL to refresh
   * @return boolean indicating whether the refresh succeeded
   */
  public boolean fbml_refreshImgSrc(URL imageUrl) throws FacebookException, IOException {
    return extractBoolean(this.callMethod(FacebookMethod.FBML_REFRESH_IMG_SRC,
                          new Pair<String, CharSequence>("url", imageUrl.toString())));
  }

  /**
   * Publishes a templatized action for the current user.  The action will appear in their minifeed,
   * and may appear in their friends' newsfeeds depending upon a number of different factors.  When
   * a template match exists between multiple distinct users (like "Bob recommends Bizou" and "Sally
   * recommends Bizou"), the feed entries may be combined in the newfeed (to something like "Bob and sally
   * recommend Bizou").  This happens automatically, and *only* if the template match between the two
   * feed entries is identical.<br />
   * <br />
   * Feed entries are not aggregated for a single user (so "Bob recommends Bizou" and "Bob recommends Le
   * Charm" *will not* become "Bob recommends Bizou and Le Charm").<br />
   * <br />
   * If the user's action involves one or more of their friends, list them in the 'targetIds' parameter.
   * For example, if you have "Bob says hi to Sally and Susie", and Sally's UID is 1, and Susie's UID is 2,
   * then pass a 'targetIds' paramters of "1,2".  If you pass this parameter, you can use the "{target}" token
   * in your templates.  Probably it also makes it more likely that Sally and Susie will see the feed entry
   * in their newsfeed, relative to any other friends Bob might have.  It may be a good idea to always send
   * a list of all the user's friends, and avoid using the "{target}" token, to maximize distribution of the
   * story through the newsfeed.<br />
   * <br />
   * The only strictly required parameter is 'titleTemplate', which must contain the "{actor}" token somewhere
   * inside of it.  All other parameters, options, and tokens are optional, and my be set to null if
   * being omitted.<br />
   * <br />
   * Not that stories will only be aggregated if *all* templates match and *all* template parameters match, so
   * if two entries have the same templateTitle and titleData, but a different bodyTemplate, they will not
   * aggregate.  Probably it's better to use bodyGeneral instead of bodyTemplate, for the extra flexibility
   * it provides.<br />
   * <br />
   * <br />
   * Note that this method is replacing 'feed_publishActionOfUser', which has been deprecated by Facebook.
   * For specific details, visit http://wiki.developers.facebook.com/index.php/Feed.publishTemplatizedAction
   *
   *
   * @param titleTemplate the template for the title of the feed entry, this must contain the "(actor}" token.
   *                      Any other tokens are optional, i.e. "{actor} recommends {place}".
   * @param titleData JSON-formatted values for any tokens used in titleTemplate, with the exception of "{actor}"
   *                  and "{target}", which Facebook populates automatically, i.e. "{place: "<a href='http://www.bizou.com'>Bizou</a>"}".
   * @param bodyTemplate the template for the body of the feed entry, works the same as 'titleTemplate', but
   *                     is not required to contain the "{actor}" token.
   * @param bodyData works the same as titleData
   * @param bodyGeneral non-templatized content for the body, may contain markup, may not contain tokens.
   * @param pictures a list of up to 4 images to display, with optional hyperlinks for each one.
   * @param targetIds a comma-seperated list of the UID's of any friend(s) who are involved in this feed
   *                  action (if there are any), this specifies the value of the "{target}" token.  If you
   *                  use this token in any of your templates, you must specify a value for this parameter.
   *
   * @return a Document representing the XML response returned from the Facebook API server.
   *
   * @throws FacebookException if any number of bad things happen
   * @throws IOException
   */
  public boolean feed_publishTemplatizedAction(String titleTemplate, String titleData, String bodyTemplate,
          String bodyData, String bodyGeneral, Collection<? extends Pair<URL, URL>> pictures, String targetIds) throws FacebookException, IOException {

      return templatizedFeedHandler(FacebookMethod.FEED_PUBLISH_TEMPLATIZED_ACTION, titleTemplate, titleData, bodyTemplate,
              bodyData, bodyGeneral, pictures, targetIds);
  }

  /**
   * Publishes a templatized action for the current user.  The action will appear in their minifeed,
   * and may appear in their friends' newsfeeds depending upon a number of different factors.  When
   * a template match exists between multiple distinct users (like "Bob recommends Bizou" and "Sally
   * recommends Bizou"), the feed entries may be combined in the newfeed (to something like "Bob and sally
   * recommend Bizou").  This happens automatically, and *only* if the template match between the two
   * feed entries is identical.<br />
   * <br />
   * Feed entries are not aggregated for a single user (so "Bob recommends Bizou" and "Bob recommends Le
   * Charm" *will not* become "Bob recommends Bizou and Le Charm").<br />
   * <br />
   * If the user's action involves one or more of their friends, list them in the 'targetIds' parameter.
   * For example, if you have "Bob says hi to Sally and Susie", and Sally's UID is 1, and Susie's UID is 2,
   * then pass a 'targetIds' paramters of "1,2".  If you pass this parameter, you can use the "{target}" token
   * in your templates.  Probably it also makes it more likely that Sally and Susie will see the feed entry
   * in their newsfeed, relative to any other friends Bob might have.  It may be a good idea to always send
   * a list of all the user's friends, and avoid using the "{target}" token, to maximize distribution of the
   * story through the newsfeed.<br />
   * <br />
   * The only strictly required parameter is 'titleTemplate', which must contain the "{actor}" token somewhere
   * inside of it.  All other parameters, options, and tokens are optional, and my be set to null if
   * being omitted.<br />
   * <br />
   * Not that stories will only be aggregated if *all* templates match and *all* template parameters match, so
   * if two entries have the same templateTitle and titleData, but a different bodyTemplate, they will not
   * aggregate.  Probably it's better to use bodyGeneral instead of bodyTemplate, for the extra flexibility
   * it provides.<br />
   * <br />
   * <br />
   * Note that this method is replacing 'feed_publishActionOfUser', which has been deprecated by Facebook.
   * For specific details, visit http://wiki.developers.facebook.com/index.php/Feed.publishTemplatizedAction
   *
   *
   * @param action a TemplatizedAction instance that represents the feed data to publish
   *
   * @return a Document representing the XML response returned from the Facebook API server.
   *
   * @throws FacebookException if any number of bad things happen
   * @throws IOException
   */
  public boolean feed_PublishTemplatizedAction(TemplatizedAction action) throws FacebookException, IOException {
      return this.feed_publishTemplatizedAction(action.getTitleTemplate(), action.getTitleParams(), action.getBodyTemplate(), action.getBodyParams(), action.getBodyGeneral(), action.getPictures(), action.getTargetIds());
  }

  /**
   * Publish the notification of an action taken by a user to newsfeed.
   * @param title the title of the feed story
   * @param body the body of the feed story
   * @param images (optional) up to four pairs of image URLs and (possibly null) link URLs
   * @param priority
   * @return a document object containing the server response
   *
   * @deprecated Facebook will be removing this API call (it is to be replaced with feed_publishTemplatizedAction)
   */
  public boolean feed_publishActionOfUser(CharSequence title, CharSequence body,
                                           Collection<? extends Pair<URL, URL>> images,
                                           Integer priority) throws FacebookException,
                                                                    IOException {
    return feedHandlerBoolean(FacebookMethod.FEED_PUBLISH_ACTION_OF_USER, title, body, images, priority);
  }

  /**
   * @see FacebookRestClient#feed_publishActionOfUser(CharSequence,CharSequence,Collection,Integer)
   *
   * @deprecated Facebook will be removing this API call (it is to be replaced with feed_publishTemplatizedAction)
   */
  public boolean feed_publishActionOfUser(String title,
                                           String body) throws FacebookException,
                                                                     IOException {
    return feed_publishActionOfUser(title, body, null, null);
  }
  
  /**
   * @see FacebookRestClient#feed_publishActionOfUser(CharSequence,CharSequence,Collection,Integer)
   *
   * @deprecated Facebook will be removing this API call (it is to be replaced with feed_publishTemplatizedAction)
   */
  public boolean feed_publishActionOfUser(CharSequence title,
                                           CharSequence body) throws FacebookException,
                                                                     IOException {
    return feed_publishActionOfUser(title, body, null, null);
  }

  /**
   * @see FacebookRestClient#feed_publishActionOfUser(CharSequence,CharSequence,Collection,Integer)
   *
   * @deprecated Facebook will be removing this API call (it is to be replaced with feed_publishTemplatizedAction)
   */
  public boolean feed_publishActionOfUser(CharSequence title, CharSequence body,
                                           Integer priority) throws FacebookException,
                                                                    IOException {
    return feed_publishActionOfUser(title, body, null, priority);
  }

  /**
   * Publish a story to the logged-in user's newsfeed.
   * @param title the title of the feed story
   * @param body the body of the feed story
   * @param images (optional) up to four pairs of image URLs and (possibly null) link URLs
   * @param priority
   * @return a Document object containing the server response
   */
  public boolean feed_publishStoryToUser(CharSequence title, CharSequence body,
                                          Collection<? extends Pair<URL, URL>> images,
                                          Integer priority) throws FacebookException, IOException {
    return feedHandlerBoolean(FacebookMethod.FEED_PUBLISH_STORY_TO_USER, title, body, images, priority);
  }

  /**
   * @see FacebookRestClient#feed_publishStoryToUser(CharSequence,CharSequence,Collection,Integer)
   */
  public boolean feed_publishStoryToUser(String title,
                                          String body) throws FacebookException,
                                                                    IOException {
    return feed_publishStoryToUser(title, body, null, null);
  }

  /**
   * @see FacebookRestClient#feed_publishStoryToUser(CharSequence,CharSequence,Collection,Integer)
   */
  public boolean feed_publishStoryToUser(String title, String body,
                                          Integer priority) throws FacebookException, IOException {
    return feed_publishStoryToUser(title, body, null, priority);
  }
  
  /**
   * @see FacebookRestClient#feed_publishStoryToUser(CharSequence,CharSequence,Collection,Integer)
   */
  public boolean feed_publishStoryToUser(CharSequence title,
                                          CharSequence body) throws FacebookException,
                                                                    IOException {
    return feed_publishStoryToUser(title, body, null, null);
  }

  /**
   * @see FacebookRestClient#feed_publishStoryToUser(CharSequence,CharSequence,Collection,Integer)
   */
  public boolean feed_publishStoryToUser(CharSequence title, CharSequence body,
                                          Integer priority) throws FacebookException, IOException {
    return feed_publishStoryToUser(title, body, null, priority);
  }

  protected Document feedHandler(FacebookMethod feedMethod, CharSequence title, CharSequence body,
                                 Collection<? extends Pair<URL, URL>> images,
                                 Integer priority) throws FacebookException, IOException {
    assert (images == null || images.size() <= 4);

    ArrayList<Pair<String, CharSequence>> params =
      new ArrayList<Pair<String, CharSequence>>(feedMethod.numParams());

    params.add(new Pair<String, CharSequence>("title", title));
    if (null != body)
    params.add(new Pair<String, CharSequence>("body", body));
    if (null != priority)
      params.add(new Pair<String, CharSequence>("priority", priority.toString()));
    if (null != images && !images.isEmpty()) {
      int image_count = 0;
      for (Pair<URL, URL> image: images) {
        ++image_count;
        assert (image.first != null);
        params.add(new Pair<String, CharSequence>(String.format("image_%d", image_count),
                                                  image.first.toString()));
        if (image.second != null)
          params.add(new Pair<String, CharSequence>(String.format("image_%d_link", image_count),
                                                    image.second.toString()));
      }
    }
    return this.callMethod(feedMethod, params);
  }

  protected boolean feedHandlerBoolean(FacebookMethod feedMethod, CharSequence title, CharSequence body,
          Collection<? extends Pair<URL, URL>> images,
          Integer priority) throws FacebookException, IOException {
      assert (images == null || images.size() <= 4);
    
      ArrayList<Pair<String, CharSequence>> params =
          new ArrayList<Pair<String, CharSequence>>(feedMethod.numParams());
    
      params.add(new Pair<String, CharSequence>("title", title));
      if (null != body)
          params.add(new Pair<String, CharSequence>("body", body));
      if (null != priority)
          params.add(new Pair<String, CharSequence>("priority", priority.toString()));
      if (null != images && !images.isEmpty()) {
          int image_count = 0;
          for (Pair<URL, URL> image: images) {
              ++image_count;
              assert (image.first != null);
              params.add(new Pair<String, CharSequence>(String.format("image_%d", image_count),
                               image.first.toString()));
              if (image.second != null)
                  params.add(new Pair<String, CharSequence>(String.format("image_%d_link", image_count),
                                 image.second.toString()));
          }
      }
      this.callMethod(feedMethod, params);
      return this.rawResponse.contains(">1<"); //a code of '1' indicates success
  }
  
  
  protected boolean templatizedFeedHandler(FacebookMethod method, String titleTemplate, String titleData, String bodyTemplate,
          String bodyData, String bodyGeneral, Collection<? extends Pair<URL, URL>> pictures, String targetIds) throws FacebookException, IOException {
      assert (pictures == null || pictures.size() <= 4);

      long actorId = this.users_getLoggedInUser();
      ArrayList<Pair<String, CharSequence>> params = new ArrayList<Pair<String, CharSequence>>(method.numParams());

      //these are always required parameters
      params.add(new Pair<String, CharSequence>("actor_id", Long.toString(actorId)));
      params.add(new Pair<String, CharSequence>("title_template", titleTemplate));

      //these are optional parameters
      if (titleData != null) {
          params.add(new Pair<String, CharSequence>("title_data", titleData));
      }
      if (bodyTemplate != null) {
          params.add(new Pair<String, CharSequence>("body_template", bodyTemplate));
          if (bodyData != null) {
              params.add(new Pair<String, CharSequence>("body_data", bodyData));
          }
      }
      if (bodyGeneral != null) {
          params.add(new Pair<String, CharSequence>("body_general", bodyGeneral));
      }
      if (pictures != null) {
          int count = 1;
          for (Pair<URL, URL> picture : pictures) {
                params.add(new Pair<String, CharSequence>("image_" + count, picture.first.toString()));
                if (picture.second != null) {
                    params.add(new Pair<String, CharSequence>("image_" + count + "_link", picture.second.toString()));
                }
                count++;
          }
      }
      if (targetIds != null) {
          params.add(new Pair<String, CharSequence>("target_ids", targetIds));
      }
      this.callMethod(method, params);
      return this.rawResponse.contains(">1<"); //a code of '1' indicates success
  }

  /**
   * Returns all visible events according to the filters specified. This may be used to find all events of a user, or to query specific eids.
   * @param eventIds filter by these event ID's (optional)
   * @param userId filter by this user only (optional)
   * @param startTime UTC lower bound (optional)
   * @param endTime UTC upper bound (optional)
   * @return Document of events
   */
  public Document events_get(Long userId, Collection<Long> eventIds, Long startTime,
                             Long endTime) throws FacebookException, IOException {
    ArrayList<Pair<String, CharSequence>> params =
      new ArrayList<Pair<String, CharSequence>>(FacebookMethod.EVENTS_GET.numParams());

    boolean hasUserId = null != userId && 0 != userId;
    boolean hasEventIds = null != eventIds && !eventIds.isEmpty();
    boolean hasStart = null != startTime && 0 != startTime;
    boolean hasEnd = null != endTime && 0 != endTime;

    if (hasUserId)
      params.add(new Pair<String, CharSequence>("uid", Long.toString(userId)));
    if (hasEventIds)
      params.add(new Pair<String, CharSequence>("eids", delimit(eventIds)));
    if (hasStart)
      params.add(new Pair<String, CharSequence>("start_time", startTime.toString()));
    if (hasEnd)
      params.add(new Pair<String, CharSequence>("end_time", endTime.toString()));
    return this.callMethod(FacebookMethod.EVENTS_GET, params);
  }

  /**
   * Retrieves the membership list of an event
   * @param eventId event id
   * @return Document consisting of four membership lists corresponding to RSVP status, with keys
   *  'attending', 'unsure', 'declined', and 'not_replied'
   */
  public Document events_getMembers(Number eventId) throws FacebookException, IOException {
    assert (null != eventId);
    return this.callMethod(FacebookMethod.EVENTS_GET_MEMBERS,
                           new Pair<String, CharSequence>("eid", eventId.toString()));
  }


  /**
   * Retrieves the friends of the currently logged in user.
   * @return array of friends
   */
  public Document friends_areFriends(long userId1, long userId2) throws FacebookException,
                                                                      IOException {
    return this.callMethod(FacebookMethod.FRIENDS_ARE_FRIENDS,
                           new Pair<String, CharSequence>("uids1", Long.toString(userId1)),
                           new Pair<String, CharSequence>("uids2", Long.toString(userId2)));
  }

  public Document friends_areFriends(Collection<Long> userIds1,
                                     Collection<Long> userIds2) throws FacebookException,
                                                                          IOException {
    assert (userIds1 != null && userIds2 != null);
    assert (!userIds1.isEmpty() && !userIds2.isEmpty());
    assert (userIds1.size() == userIds2.size());

    return this.callMethod(FacebookMethod.FRIENDS_ARE_FRIENDS,
                           new Pair<String, CharSequence>("uids1", delimit(userIds1)),
                           new Pair<String, CharSequence>("uids2", delimit(userIds2)));
  }

  /**
   * Retrieves the friends of the currently logged in user.
   * @return array of friends
   */
  public Document friends_get() throws FacebookException, IOException {
    return this.callMethod(FacebookMethod.FRIENDS_GET);
  }

  /**
   * Retrieves the friends of the currently logged in user, who are also users
   * of the calling application.
   * @return array of friends
   */
  public Document friends_getAppUsers() throws FacebookException, IOException {
    return this.callMethod(FacebookMethod.FRIENDS_GET_APP_USERS);
  }

  /**
   * Retrieves the requested info fields for the requested set of users.
   * @param userIds a collection of user IDs for which to fetch info
   * @param fields a set of ProfileFields
   * @return a Document consisting of a list of users, with each user element
   *   containing the requested fields.
   */
  public Document users_getInfo(Collection<Long> userIds,
                                EnumSet<ProfileField> fields) throws FacebookException,
                                                                     IOException {
    // assertions test for invalid params
    assert (userIds != null);
    assert (fields != null);
    assert (!fields.isEmpty());

    return this.callMethod(FacebookMethod.USERS_GET_INFO,
                           new Pair<String, CharSequence>("uids", delimit(userIds)),
                           new Pair<String, CharSequence>("fields", delimit(fields)));
  }

  /**
   * Retrieves the requested info fields for the requested set of users.
   * @param userIds a collection of user IDs for which to fetch info
   * @param fields a set of strings describing the info fields desired, such as "last_name", "sex"
   * @return a Document consisting of a list of users, with each user element
   *   containing the requested fields.
   */
  public Document users_getInfo(Collection<Long> userIds,
                                Set<CharSequence> fields) throws FacebookException, IOException {
    // assertions test for invalid params
    assert (userIds != null);
    assert (fields != null);
    assert (!fields.isEmpty());

    return this.callMethod(FacebookMethod.USERS_GET_INFO,
                           new Pair<String, CharSequence>("uids", delimit(userIds)),
                           new Pair<String, CharSequence>("fields", delimit(fields)));
  }

  /**
   * Retrieves the user ID of the user logged in to this API session
   * @return the Facebook user ID of the logged-in user
   */
  public long users_getLoggedInUser() throws FacebookException, IOException {
    Document d = this.callMethod(FacebookMethod.USERS_GET_LOGGED_IN_USER);
    return Long.parseLong(d.getFirstChild().getTextContent());
  }

  /**
   * Retrieves an indicator of whether the logged-in user has installed the
   * application associated with the _apiKey.
   * @return boolean indicating whether the user has installed the app
   */
  public boolean users_isAppAdded() throws FacebookException, IOException {
    return extractBoolean(this.callMethod(FacebookMethod.USERS_IS_APP_ADDED));
  }

  /**
   * Used to retrieve photo objects using the search parameters (one or more of the
   * parameters must be provided).
   *
   * @param subjId retrieve from photos associated with this user (optional).
   * @param albumId retrieve from photos from this album (optional)
   * @param photoIds retrieve from this list of photos (optional)
   *
   * @return an Document of photo objects.
   */
  public Document photos_get(Long subjId, Long albumId,
                             Collection<Long> photoIds) throws FacebookException, IOException {
    ArrayList<Pair<String, CharSequence>> params =
      new ArrayList<Pair<String, CharSequence>>(FacebookMethod.PHOTOS_GET.numParams());

    boolean hasUserId = null != subjId && 0 != subjId;
    boolean hasAlbumId = null != albumId && 0 != albumId;
    boolean hasPhotoIds = null != photoIds && !photoIds.isEmpty();
    assert (hasUserId || hasAlbumId || hasPhotoIds);

    if (hasUserId)
      params.add(new Pair<String, CharSequence>("subj_id", Long.toString(subjId)));
    if (hasAlbumId)
      params.add(new Pair<String, CharSequence>("aid", Long.toString(albumId)));
    if (hasPhotoIds)
      params.add(new Pair<String, CharSequence>("pids", delimit(photoIds)));

    return this.callMethod(FacebookMethod.PHOTOS_GET, params);
  }

  public Document photos_get(Long albumId, Collection<Long> photoIds, boolean album) throws FacebookException,
                                                                             IOException {
    return photos_get(null/*subjId*/, albumId, photoIds);
  }

  public Document photos_get(Long subjId, Collection<Long> photoIds) throws FacebookException,
                                                                               IOException {
    return photos_get(subjId, null/*albumId*/, photoIds);
  }

  public Document photos_get(Long subjId, Long albumId) throws FacebookException, IOException {
    return photos_get(subjId, albumId, null/*photoIds*/);
  }

  public Document photos_get(Collection<Long> photoIds) throws FacebookException, IOException {
    return photos_get(null/*subjId*/, null/*albumId*/, photoIds);
  }

  public Document photos_get(Long albumId, boolean album) throws FacebookException, IOException {
    return photos_get(null/*subjId*/, albumId, null/*photoIds*/);
  }

  public Document photos_get(Long subjId) throws FacebookException, IOException {
    return photos_get(subjId, null/*albumId*/, null/*photoIds*/);
  }

  /**
   * Retrieves album metadata. Pass a user id and/or a list of album ids to specify the albums
   * to be retrieved (at least one must be provided)
   *
   * @param userId retrieve metadata for albums created the id of the user whose album you wish  (optional).
   * @param albumIds the ids of albums whose metadata is to be retrieved
   * @return album objects.
   */
  public Document photos_getAlbums(Long userId,
                                   Collection<Long> albumIds) throws FacebookException,
                                                                     IOException {
    boolean hasUserId = null != userId && userId != 0;
    boolean hasAlbumIds = null != albumIds && !albumIds.isEmpty();
    assert (hasUserId || hasAlbumIds); // one of the two must be provided

    if (hasUserId)
      return (hasAlbumIds) ?
             this.callMethod(FacebookMethod.PHOTOS_GET_ALBUMS, new Pair<String, CharSequence>("uid",
                                                                                              Long.toString(userId)),
                             new Pair<String, CharSequence>("aids", delimit(albumIds))) :
             this.callMethod(FacebookMethod.PHOTOS_GET_ALBUMS,
                             new Pair<String, CharSequence>("uid", Long.toString(userId)));
    else
      return this.callMethod(FacebookMethod.PHOTOS_GET_ALBUMS,
                             new Pair<String, CharSequence>("aids", delimit(albumIds)));
  }

  public Document photos_getAlbums(Long userId) throws FacebookException, IOException {
    return photos_getAlbums(userId, null /*albumIds*/);
  }

  public Document photos_getAlbums(Collection<Long> albumIds) throws FacebookException,
                                                                     IOException {
    return photos_getAlbums(null /*userId*/, albumIds);
  }

  /**
   * Retrieves the tags for the given set of photos.
   * @param photoIds The list of photos from which to extract photo tags.
   * @return the created album
   */
  public Document photos_getTags(Collection<Long> photoIds) throws FacebookException, IOException {
    return this.callMethod(FacebookMethod.PHOTOS_GET_TAGS,
                           new Pair<String, CharSequence>("pids", delimit(photoIds)));
  }

  /**
   * Creates an album.
   * @param albumName The list of photos from which to extract photo tags.
   * @return the created album
   */
  public Document photos_createAlbum(String albumName) throws FacebookException, IOException {
    return this.photos_createAlbum(albumName, null/*description*/, null/*location*/);
  }

  /**
   * Creates an album.
   * @param name The album name.
   * @param location The album location (optional).
   * @param description The album description (optional).
   * @return an array of photo objects.
   */
  public Document photos_createAlbum(String name, String description,
                                     String location) throws FacebookException, IOException {
    assert (null != name && !"".equals(name));
    ArrayList<Pair<String, CharSequence>> params =
      new ArrayList<Pair<String, CharSequence>>(FacebookMethod.PHOTOS_CREATE_ALBUM.numParams());
    params.add(new Pair<String, CharSequence>("name", name));
    if (null != description)
      params.add(new Pair<String, CharSequence>("description", description));
    if (null != location)
      params.add(new Pair<String, CharSequence>("location", location));
    return this.callMethod(FacebookMethod.PHOTOS_CREATE_ALBUM, params);
  }

  /**
   * Adds several tags to a photo.
   * @param photoId The photo id of the photo to be tagged.
   * @param tags A list of PhotoTags.
   * @return a list of booleans indicating whether the tag was successfully added.
   */
  public Document photos_addTags(Long photoId, Collection<PhotoTag> tags)
    throws FacebookException, IOException {
    assert (photoId > 0);
    assert (null != tags && !tags.isEmpty());
    String tagStr = null;
    try {
        JSONArray jsonTags=new JSONArray();
        for (PhotoTag tag : tags) {
          jsonTags.put(tag.jsonify());
        }
        tagStr = jsonTags.toString();
    }
    catch (Exception ignored) {}

    return this.callMethod(FacebookMethod.PHOTOS_ADD_TAG,
                           new Pair<String, CharSequence>("pid", photoId.toString()),
                           new Pair<String, CharSequence>("tags", tagStr));
  }

  /**
   * Adds a tag to a photo.
   * @param photoId The photo id of the photo to be tagged.
   * @param xPct The horizontal position of the tag, as a percentage from 0 to 100, from the left of the photo.
   * @param yPct The vertical position of the tag, as a percentage from 0 to 100, from the top of the photo.
   * @param taggedUserId The list of photos from which to extract photo tags.
   * @return whether the tag was successfully added.
   */
  public boolean photos_addTag(Long photoId, Long taggedUserId, Double xPct,
                               Double yPct) throws FacebookException, IOException {
    return photos_addTag(photoId, xPct, yPct, taggedUserId, null);
  }

  /**
   * Adds a tag to a photo.
   * @param photoId The photo id of the photo to be tagged.
   * @param xPct The horizontal position of the tag, as a percentage from 0 to 100, from the left of the photo.
   * @param yPct The list of photos from which to extract photo tags.
   * @param tagText The text of the tag.
   * @return whether the tag was successfully added.
   */
  public boolean photos_addTag(Long photoId, CharSequence tagText, Double xPct,
                               Double yPct) throws FacebookException, IOException {
    return photos_addTag(photoId, xPct, yPct, null, tagText);
  }

  private boolean photos_addTag(Long photoId, Double xPct, Double yPct, Long taggedUserId,
                                CharSequence tagText) throws FacebookException, IOException {
    assert (null != photoId && !photoId.equals(0));
    assert (null != taggedUserId || null != tagText);
    assert (null != xPct && xPct >= 0 && xPct <= 100);
    assert (null != yPct && yPct >= 0 && yPct <= 100);
    Document d =
      this.callMethod(FacebookMethod.PHOTOS_ADD_TAG, new Pair<String, CharSequence>("pid",
                                                                                    photoId.toString()),
                      new Pair<String, CharSequence>("tag_uid", taggedUserId.toString()),
                      new Pair<String, CharSequence>("x", xPct.toString()),
                      new Pair<String, CharSequence>("y", yPct.toString()));
    return extractBoolean(d);
  }

  public Document photos_upload(File photo) throws FacebookException, IOException {
    return /* caption */ /* albumId */photos_upload(photo, null, null);
  }

  public Document photos_upload(File photo, String caption) throws FacebookException, IOException {
    return /* albumId */photos_upload(photo, caption, null);
  }

  public Document photos_upload(File photo, Long albumId) throws FacebookException, IOException {
    return /* caption */photos_upload(photo, null, albumId);
  }

  public Document photos_upload(File photo, String caption, Long albumId) throws FacebookException,
                                                                                 IOException {
    ArrayList<Pair<String, CharSequence>> params =
      new ArrayList<Pair<String, CharSequence>>(FacebookMethod.PHOTOS_UPLOAD.numParams());
    assert (photo.exists() && photo.canRead());
    this._uploadFile = photo;
    if (null != albumId)
      params.add(new Pair<String, CharSequence>("aid", Long.toString(albumId)));
    if (null != caption)
      params.add(new Pair<String, CharSequence>("caption", caption));
    return callMethod(FacebookMethod.PHOTOS_UPLOAD, params);
  }

  /**
   * Retrieves the groups associated with a user
   * @param userId Optional: User associated with groups.
   *  A null parameter will default to the session user.
   * @param groupIds Optional: group ids to query.
   *   A null parameter will get all groups for the user.
   * @return array of groups
   */
  public Document groups_get(Long userId, Collection<Long> groupIds) throws FacebookException,
                                                                               IOException {
    boolean hasGroups = (null != groupIds && !groupIds.isEmpty());
    if (null != userId)
      return hasGroups ?
             this.callMethod(FacebookMethod.GROUPS_GET, new Pair<String, CharSequence>("uid",
                                                                                       userId.toString()),
                             new Pair<String, CharSequence>("gids", delimit(groupIds))) :
             this.callMethod(FacebookMethod.GROUPS_GET,
                             new Pair<String, CharSequence>("uid", userId.toString()));
    else
      return hasGroups ?
             this.callMethod(FacebookMethod.GROUPS_GET, new Pair<String, CharSequence>("gids",
                                                                                       delimit(groupIds))) :
             this.callMethod(FacebookMethod.GROUPS_GET);
  }

  /**
   * Retrieves the membership list of a group
   * @param groupId the group id
   * @return a Document containing four membership lists of
   *  'members', 'admins', 'officers', and 'not_replied'
   */
  public Document groups_getMembers(Number groupId) throws FacebookException, IOException {
    assert (null != groupId);
    return this.callMethod(FacebookMethod.GROUPS_GET_MEMBERS,
                           new Pair<String, CharSequence>("gid", groupId.toString()));
  }

  /**
   * Retrieves the results of a Facebook Query Language query
   * @param query : the FQL query statement
   * @return varies depending on the FQL query
   */
  public Document fql_query(CharSequence query) throws FacebookException, IOException {
    assert (null != query);
    return this.callMethod(FacebookMethod.FQL_QUERY,
                           new Pair<String, CharSequence>("query", query));
  }

  /**
   * Retrieves the outstanding notifications for the session user.
   * @return a Document containing
   *  notification count pairs for 'messages', 'pokes' and 'shares',
   *  a uid list of 'friend_requests', a gid list of 'group_invites',
   *  and an eid list of 'event_invites'
   */
  public Document notifications_get() throws FacebookException, IOException {
    return this.callMethod(FacebookMethod.NOTIFICATIONS_GET);
  }

  /**
   * Send a request or invitations to the specified users.
   * @param recipientIds the user ids to which the request is to be sent
   * @param type the type of request/invitation - e.g. the word "event" in "1 event invitation."
   * @param content Content of the request/invitation. This should be FBML containing only links and the
   *   special tag <fb:req-choice url="" label="" /> to specify the buttons to be included in the request.
   * @param image URL of an image to show beside the request. It will be resized to be 100 pixels wide.
   * @param isInvite whether this is a "request" or an "invite"
   * @return a URL, possibly null, to which the user should be redirected to finalize
   *    the sending of the message
   *
   * @deprecated this method has been removed from the Facebook API server
   */
  public URL notifications_sendRequest(Collection<Long> recipientIds, CharSequence type,
  CharSequence content, URL image, boolean isInvite) throws FacebookException, IOException {
    assert (null != recipientIds && !recipientIds.isEmpty());
    assert (null != type);
    assert (null != content);
    assert (null != image);

    Document d =
      this.callMethod(FacebookMethod.NOTIFICATIONS_SEND_REQUEST,
                      new Pair<String, CharSequence>("to_ids", delimit(recipientIds)),
                      new Pair<String, CharSequence>("type", type),
                      new Pair<String, CharSequence>("content", content),
                      new Pair<String, CharSequence>("image", image.toString()),
                      new Pair<String, CharSequence>("invite", isInvite ? "1" : "0"));
    String url = d.getFirstChild().getTextContent();
    return (null == url || "".equals(url)) ? null : new URL(url);
  }

  /**
   * Send a notification message to the specified users.
   * @param recipientIds the user ids to which the message is to be sent
   * @param notification the notification to send, this is delivered to the targets' Facebook account(s)
   * @param email the email to send, this is delivered to the targets' external e-mail account(s)
   * @return a URL, possibly null, to which the user should be redirected to finalize
   *    the sending of the message
   */
  public URL notifications_send(Collection<Long> recipientIds,
                                CharSequence notification,
                                CharSequence email) throws FacebookException, IOException {
    assert (null != recipientIds && !recipientIds.isEmpty());
    assert (null != notification);
    Document d;

    if (email != null) {
        d = this.callMethod(FacebookMethod.NOTIFICATIONS_SEND,
                      new Pair<String, CharSequence>("to_ids", delimit(recipientIds)),
                      new Pair<String, CharSequence>("notification", notification),
                      new Pair<String, CharSequence>("email", email));
    }
    else {
        d = this.callMethod(FacebookMethod.NOTIFICATIONS_SEND,
                new Pair<String, CharSequence>("to_ids", delimit(recipientIds)),
                new Pair<String, CharSequence>("notification", notification));
    }
    String url = d.getFirstChild().getTextContent();
    return (null == url || "".equals(url)) ? null : new URL(url);
  }

  protected static boolean extractBoolean(Document doc) {
    String content = doc.getFirstChild().getTextContent();
    return 1 == Integer.parseInt(content);
  }

  public InputStream postFileRequest(String methodName,
                                     Map<String, CharSequence> params) {
    assert (null != _uploadFile);
    try {
      BufferedInputStream bufin = new BufferedInputStream(new FileInputStream(_uploadFile));

      String boundary = Long.toString(System.currentTimeMillis(), 16);
      URLConnection con = SERVER_URL.openConnection();
      con.setDoInput(true);
      con.setDoOutput(true);
      con.setUseCaches(false);
      con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
      con.setRequestProperty("MIME-version", "1.0");

      DataOutputStream out = new DataOutputStream(con.getOutputStream());

      for (Map.Entry<String, CharSequence> entry: params.entrySet()) {
        out.writeBytes(PREF + boundary + CRLF);
        out.writeBytes("Content-disposition: form-data; name=\"" + entry.getKey() + "\"");
        out.writeBytes(CRLF + CRLF);
        out.writeBytes(entry.getValue().toString());
        out.writeBytes(CRLF);
      }

      out.writeBytes(PREF + boundary + CRLF);
      out.writeBytes("Content-disposition: form-data; filename=\"" + _uploadFile.getName() + "\"" +
                     CRLF);
      out.writeBytes("Content-Type: image/jpeg" + CRLF);
      // out.writeBytes("Content-Transfer-Encoding: binary" + CRLF); // not necessary

      // Write the file
      out.writeBytes(CRLF);
      byte b[] = new byte[UPLOAD_BUFFER_SIZE];
      int byteCounter = 0;
      int i;
      while (-1 != (i = bufin.read(b))) {
        byteCounter += i;
        out.write(b, 0, i);
      }
      out.writeBytes(CRLF + PREF + boundary + PREF + CRLF);

      out.flush();
      out.close();

      InputStream is = con.getInputStream();
      return is;
    }
    catch (Exception e) {
        System.err.println("caught exception: " + e);
        e.printStackTrace();
        return null;
    }
  }

  /**
   * Call this function and store the result, using it to generate the
   * appropriate login url and then to retrieve the session information.
   * @return String the auth_token string
   */
  public String auth_createToken() throws FacebookException, IOException {
    Document d = this.callMethod(FacebookMethod.AUTH_CREATE_TOKEN);
    return d.getFirstChild().getTextContent();
  }

  /**
   * Call this function to retrieve the session information after your user has
   * logged in.
   * @param authToken the token returned by auth_createToken or passed back to your callback_url.
   */
  public String auth_getSession(String authToken) throws FacebookException, IOException {
    Document d =
      this.callMethod(FacebookMethod.AUTH_GET_SESSION, new Pair<String, CharSequence>("auth_token",
                                                                                      authToken.toString()));
    this._sessionKey =
        d.getElementsByTagName("session_key").item(0).getFirstChild().getTextContent();
    this._userId =
        Long.parseLong(d.getElementsByTagName("uid").item(0).getFirstChild().getTextContent());
    if (this._isDesktop)
      this._sessionSecret =
          d.getElementsByTagName("secret").item(0).getFirstChild().getTextContent();
    return this._sessionKey;
  }

  /**
   * Returns a JAXB object of the type that corresponds to the last API call made on the client.  Each
   * Facebook Platform API call that returns a Document object has a JAXB response object associated
   * with it.  The naming convention is generally intuitive.  For example, if you invoke the
   * 'user_getInfo' API call, the associated JAXB response object is 'UsersGetInfoResponse'.<br />
   * <br />
   * An example of how to use this method:<br />
   *  <br />
   *    FacebookRestClient client = new FacebookRestClient("apiKey", "secretKey", "sessionId");<br />
   *    client.friends_get();<br />
   *    FriendsGetResponse response = (FriendsGetResponse)client.getResponsePOJO();<br />
   *    List<Long> friends = response.getUid(); <br />
   * <br />
   * This is particularly useful in the case of API calls that return a Document object, as working
   * with the JAXB response object is generally much simple than trying to walk/parse the DOM by
   * hand.<br />
   * <br />
   * This method can be safely called multiple times, though note that it will only return the
   * response-object corresponding to the most recent Facebook Platform API call made.<br />
   * <br />
   * Note that you must cast the return value of this method to the correct type in order to do anything
   * useful with it.
   *
   * @return a JAXB POJO ("Plain Old Java Object") of the type that corresponds to the last API call made on
   *         the client.  Note that you must cast this object to its proper type before you will be able to
   *         do anything useful with it.
   */
  public Object getResponsePOJO(){
      if (this.rawResponse == null) {
          return null;
      }
      JAXBContext jc;
      Object pojo = null;
      try {
          jc = JAXBContext.newInstance("com.facebook.api.schema");
          Unmarshaller unmarshaller = jc.createUnmarshaller();
          pojo =  unmarshaller.unmarshal(new ByteArrayInputStream(this.rawResponse.getBytes("UTF-8")));
      } catch (JAXBException e) {
          System.err.println("getResponsePOJO() - Could not unmarshall XML stream into POJO");
          e.printStackTrace();
      }
      catch (NullPointerException e) {
          System.err.println("getResponsePOJO() - Could not unmarshall XML stream into POJO.");
          e.printStackTrace();
      } catch (UnsupportedEncodingException e) {
          System.err.println("getResponsePOJO() - Could not unmarshall XML stream into POJO.");
          e.printStackTrace();
      }
      return pojo;
  }

  /**
   * Lookup a single preference value for the current user.
   *
   * @param prefId the id of the preference to lookup.  This should be an integer value from 0-200.
   *
   * @return The value of that preference, or null if it is not yet set.
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public String data_getUserPreference(Integer prefId) throws FacebookException, IOException {
      if ((prefId < 0) || (prefId > 200)) {
          throw new FacebookException(ErrorCode.GEN_INVALID_PARAMETER, "The preference id must be an integer value from 0-200.");
      }
      this.callMethod(FacebookMethod.DATA_GET_USER_PREFERENCE, new Pair<String, CharSequence>("pref_id", Integer.toString(prefId)));
      this.checkError();

      if (! this.rawResponse.contains("</data_getUserPreference_response>")) {
          //there is no value set for this preference yet
          return null;
      }
      String result = this.rawResponse.substring(0, this.rawResponse.indexOf("</data_getUserPreference_response>"));
      result = result.substring(result.indexOf("facebook.xsd\">") + "facebook.xsd\">".length());

      return reconstructValue(result);
  }

  /**
   * Get a map containing all preference values set for the current user.
   *
   * @return a map of preference values, keyed by preference id.  The map will contain all
   *         preferences that have been set for the current user.  If there are no preferences
   *         currently set, the map will be empty.  The map returned will never be null.
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public Map<Integer, String> data_getUserPreferences() throws FacebookException, IOException {
      Document response = this.callMethod(FacebookMethod.DATA_GET_USER_PREFERENCES);
      this.checkError();

      Map<Integer, String> results = new HashMap<Integer, String>();
      NodeList ids = response.getElementsByTagName("pref_id");
      NodeList values = response.getElementsByTagName("value");
      for (int count = 0; count < ids.getLength(); count++) {
          results.put(Integer.parseInt(ids.item(count).getFirstChild().getTextContent()),
                  reconstructValue(values.item(count).getFirstChild().getTextContent()));
      }

      return results;
  }

  private void checkError() throws FacebookException {
      if (this.rawResponse.contains("error_response")) {
          //<error_code>xxx</error_code>
          Integer code = Integer.parseInt(this.rawResponse.substring(this.rawResponse.indexOf("<error_code>") + "<error_code>".length(),
                  this.rawResponse.indexOf("</error_code>") + "</error_code>".length()));
          throw new FacebookException(code, "The request could not be completed!");
      }
  }

  private String reconstructValue(String input) {
      if ((input == null) || ("".equals(input))) {
          return null;
      }
      if (input.charAt(0) == '_') {
          return input.substring(1);
      }
      return input;
  }

  /**
   * Set a user-preference value.  The value can be any string up to 127 characters in length,
   * while the preference id can only be an integer between 0 and 200.  Any preference set applies
   * only to the current user of the application.
   *
   * To clear a user-preference, specify null as the value parameter.  The values of "0" and "" will
   * be stored as user-preferences with a literal value of "0" and "" respectively.
   *
   * @param prefId the id of the preference to set, an integer between 0 and 200.
   * @param value the value to store, a String of up to 127 characters in length.
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public void data_setUserPreference(Integer prefId, String value) throws FacebookException, IOException {
      if ((prefId < 0) || (prefId > 200)) {
          throw new FacebookException(ErrorCode.GEN_INVALID_PARAMETER, "The preference id must be an integer value from 0-200.");
      }
      if ((value != null) && (value.length() > 127)) {
          throw new FacebookException(ErrorCode.GEN_INVALID_PARAMETER, "The preference value cannot be longer than 128 characters.");
      }

      value = normalizePreferenceValue(value);

      Collection<Pair<String, CharSequence>> params = new ArrayList<Pair<String, CharSequence>>();
      params.add(new Pair<String, CharSequence>("pref_id", Integer.toString(prefId)));
      params.add(new Pair<String, CharSequence>("value", value));
      this.callMethod(FacebookMethod.DATA_SET_USER_PREFERENCE, params);
      this.checkError();
  }

  /**
   * Set multiple user-preferences values.  The values can be strings up to 127 characters in length,
   * while the preference id can only be an integer between 0 and 200.  Any preferences set apply
   * only to the current user of the application.
   *
   * To clear a user-preference, specify null as its value in the map.  The values of "0" and "" will
   * be stored as user-preferences with a literal value of "0" and "" respectively.
   *
   * @param value the values to store, specified in a map. The keys should be preference-id values from 0-200, and
   *              the values should be strings of up to 127 characters in length.
   * @param replace set to true if you want to remove any pre-existing preferences before writing the new ones
   *                set to false if you want the new preferences to be merged with any pre-existing preferences
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public void data_setUserPreferences(Map<Integer, String> values, boolean replace) throws FacebookException, IOException {
      JSONObject map = new JSONObject();

      for (Integer key : values.keySet()) {
          if ((key < 0) || (key > 200)) {
              throw new FacebookException(ErrorCode.GEN_INVALID_PARAMETER, "The preference id must be an integer value from 0-200.");
          }
          if ((values.get(key) != null) && (values.get(key).length() > 127)) {
              throw new FacebookException(ErrorCode.GEN_INVALID_PARAMETER, "The preference value cannot be longer than 128 characters.");
          }
          try {
              map.put(Integer.toString(key), normalizePreferenceValue(values.get(key)));
          }
          catch (JSONException e) {
              FacebookException ex = new FacebookException(ErrorCode.GEN_INVALID_PARAMETER, "Error when translating {key="
                      + key + ", value=" + values.get(key) + "}to JSON!");
              ex.setStackTrace(e.getStackTrace());
              throw ex;
          }
      }

      Collection<Pair<String, CharSequence>> params = new ArrayList<Pair<String, CharSequence>>();
      params.add(new Pair<String, CharSequence>("values", map.toString()));
      if (replace) {
          params.add(new Pair<String, CharSequence>("replace", "true"));
      }

      this.callMethod(FacebookMethod.DATA_SET_USER_PREFERENCES, params);
      this.checkError();
  }

  private String normalizePreferenceValue(String input) {
      if (input == null) {
          return "0";
      }
      return "_" + input;
  }

  /**
   * Check to see if the application is permitted to send SMS messages to the current application user.
   *
   * @return true if the application is presently able to send SMS messages to the current user
   *         false otherwise
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public boolean sms_canSend() throws FacebookException, IOException {
      return sms_canSend(this.users_getLoggedInUser());
  }

  /**
   * Check to see if the application is permitted to send SMS messages to the specified user.
   *
   * @param userId the UID of the user to check permissions for
   *
   * @return true if the application is presently able to send SMS messages to the specified user
   *         false otherwise
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public boolean sms_canSend(Long userId) throws FacebookException, IOException {
      this.callMethod(FacebookMethod.SMS_CAN_SEND, new Pair<String, CharSequence>("uid", userId.toString()));
      return this.rawResponse.contains(">0<");  //a status code of "0" indicates that the app can send messages
  }

  /**
   * Send an SMS message to the current application user.
   *
   * @param message the message to send.
   * @param smsSessionId the SMS session id to use, note that that is distinct from the user's facebook session id.  It is used to
   *                     allow applications to keep track of individual SMS conversations/threads for a single user.  Specify
   *                     null if you do not want/need to use a session for the current message.
   * @param makeNewSession set to true to request that Facebook allocate a new SMS session id for this message.  The allocated
   *                       id will be returned as the result of this API call.  You should only set this to true if you are
   *                       passing a null 'smsSessionId' value.  Otherwise you already have a SMS session id, and do not need a new one.
   *
   * @return an integer specifying the value of the session id alocated by Facebook, if one was requested.  If a new session id was
   *                    not requested, this method will return null.
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public Integer sms_send(String message, Integer smsSessionId, boolean makeNewSession) throws FacebookException, IOException {
      return sms_send(this.users_getLoggedInUser(), message, smsSessionId, makeNewSession);
  }

  /**
   * Send an SMS message to the specified user.
   *
   * @param userId the id of the user to send the message to.
   * @param message the message to send.
   * @param smsSessionId the SMS session id to use, note that that is distinct from the user's facebook session id.  It is used to
   *                     allow applications to keep track of individual SMS conversations/threads for a single user.  Specify
   *                     null if you do not want/need to use a session for the current message.
   * @param makeNewSession set to true to request that Facebook allocate a new SMS session id for this message.  The allocated
   *                       id will be returned as the result of this API call.  You should only set this to true if you are
   *                       passing a null 'smsSessionId' value.  Otherwise you already have a SMS session id, and do not need a new one.
   *
   * @return an integer specifying the value of the session id alocated by Facebook, if one was requested.  If a new session id was
   *                    not requested, this method will return null.
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public Integer sms_send(Long userId, String message, Integer smsSessionId, boolean makeNewSession) throws FacebookException, IOException {
      Collection<Pair<String, CharSequence>> params = new ArrayList<Pair<String, CharSequence>>();
      params.add(new Pair<String, CharSequence>("uid", userId.toString()));
      params.add(new Pair<String, CharSequence>("message", message));
      if (smsSessionId != null) {
          params.add(new Pair<String, CharSequence>("session_id", smsSessionId.toString()));
      }
      if (makeNewSession) {
          params.add(new Pair<String, CharSequence>("req_session", "true"));
      }

      this.callMethod(FacebookMethod.SMS_SEND, params);

      //XXX:  needs testing to make sure it's correct (Facebook always gives me a code 270 permissions error no matter what I do)
      Integer response = null;
      if ((this.rawResponse.indexOf("</sms") != -1) && (makeNewSession)) {
          String result = this.rawResponse.substring(0, this.rawResponse.indexOf("</sms"));
          result = result.substring(result.lastIndexOf(">") + 1);
          response = Integer.parseInt(result);
      }

      return response;
  }

  /**
   * Check to see if the user has granted the app a specific external permission.  In order to be granted a
   * permission, an application must direct the user to a URL of the form:
   *
   * http://www.facebook.com/authorize.php?api_key=[YOUR_API_KEY]&v=1.0&ext_perm=[PERMISSION NAME]
   *
   * @param perm the permission to check for
   *
   * @return true if the user has granted the application the specified permission
   *         false otherwise
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public boolean users_hasAppPermission(Permission perm) throws FacebookException, IOException {
      this.callMethod(FacebookMethod.USERS_HAS_PERMISSION, new Pair<String, CharSequence>("ext_perm", perm.getName()));
      return this.rawResponse.contains(">1<");  //a code of '1' is sent back to indicate that the user has the request permission
  }

  /**
   * Set the user's profile status message.  This requires that the user has granted the application the
   * 'status_update' permission, otherwise the call will return an error.  You can use 'users_hasAppPermission'
   * to check to see if the user has granted your app the abbility to update their status.
   *
   * @param newStatus the new status message to set.
   * @param clear whether or not to clear the old status message.
   *
   * @return true if the call succeeds
   *         false otherwise
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public boolean users_setStatus(String newStatus, boolean clear) throws FacebookException, IOException {
      Collection<Pair<String, CharSequence>> params = new ArrayList<Pair<String, CharSequence>>();

      if (newStatus != null) {
          params.add(new Pair<String, CharSequence>("status", newStatus));
      }
      if (clear) {
          params.add(new Pair<String, CharSequence>("clear", "true"));
      }

      this.callMethod(FacebookMethod.USERS_SET_STATUS, params);

      return this.rawResponse.contains(">1<"); //a code of '1' is sent back to indicate that the request was successful, any other response indicates error
  }

  /**
   * Associates the specified FBML markup with the specified handle/id.  The markup can then be referenced using the fb:ref FBML
   * tag, to allow a given snippet to be reused easily across multiple users, and also to allow the application to update
   * the fbml for multiple users more easily without having to make a seperate call for each user, by just changing the FBML
   * markup that is associated with the handle/id.
   *
   * @param handle the id to associate the specified markup with.  Put this in fb:ref FBML tags to reference your markup.
   * @param markup the FBML markup to store.
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public void fbml_setRefHandle(String handle, String markup) throws FacebookException, IOException {
      if ((handle == null) || ("".equals(handle))) {
          throw new FacebookException(ErrorCode.GEN_INVALID_PARAMETER, "The FBML handle may not be null or empty!");
      }
      if (markup == null) {
          markup = "";
      }
      Collection<Pair<String, CharSequence>> params = new ArrayList<Pair<String, CharSequence>>();
      params.add(new Pair<String, CharSequence>("handle", handle));
      params.add(new Pair<String, CharSequence>("fbml", markup));

      this.callMethod(FacebookMethod.FBML_SET_REF_HANDLE, params);
  }

  /**
   * Create a new marketplace listing, or modify an existing one.
   *
   * @param listingId the id of the listing to modify, set to 0 (or null) to create a new listing.
   * @param showOnProfile set to true to show the listing on the user's profile (Facebook appears to ignore this setting).
   * @param attributes JSON-encoded attributes for this listing.
   *
   * @return the id of the listing created (or modified).
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public Long marketplace_createListing(Long listingId, boolean showOnProfile, String attributes) throws FacebookException, IOException {
     if (listingId == null) {
         listingId = 0l;
     }
     MarketListing test = new MarketListing(attributes);
     if (!test.verify()) {
         throw new FacebookException(ErrorCode.GEN_INVALID_PARAMETER, "The specified listing is invalid!");
     }

     Collection<Pair<String, CharSequence>> params = new ArrayList<Pair<String, CharSequence>>();
     params.add(new Pair<String, CharSequence>("listing_id", listingId.toString()));
     if (showOnProfile) {
         params.add(new Pair<String, CharSequence>("show_on_profile", "true"));
     }
     params.add(new Pair<String, CharSequence>("listing_attrs", attributes));

     this.callMethod(FacebookMethod.MARKET_CREATE_LISTING, params);
     String result = this.rawResponse.substring(0, this.rawResponse.indexOf("</marketplace"));
     result = result.substring(result.lastIndexOf(">") + 1);
     return Long.parseLong(result);
  }

  /**
   * Create a new marketplace listing, or modify an existing one.
   *
   * @param listingId the id of the listing to modify, set to 0 (or null) to create a new listing.
   * @param showOnProfile set to true to show the listing on the user's profile, set to false to prevent the listing from being shown on the profile.
   * @param listing the listing to publish.
   *
   * @return the id of the listing created (or modified).
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public Long marketplace_createListing(Long listingId, boolean showOnProfile, MarketListing listing) throws FacebookException, IOException {
      return this.marketplace_createListing(listingId, showOnProfile, listing.getAttribs());
  }

  /**
   * Create a new marketplace listing.
   *
   * @param showOnProfile set to true to show the listing on the user's profile, set to false to prevent the listing from being shown on the profile.
   * @param listing the listing to publish.
   *
   * @return the id of the listing created (or modified).
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public Long marketplace_createListing(boolean showOnProfile, MarketListing listing) throws FacebookException, IOException {
      return this.marketplace_createListing(0l, showOnProfile, listing.getAttribs());
  }

  /**
   * Create a new marketplace listing, or modify an existing one.
   *
   * @param listingId the id of the listing to modify, set to 0 (or null) to create a new listing.
   * @param showOnProfile set to true to show the listing on the user's profile, set to false to prevent the listing from being shown on the profile.
   * @param listing the listing to publish.
   *
   * @return the id of the listing created (or modified).
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public Long marketplace_createListing(Long listingId, boolean showOnProfile, JSONObject listing) throws FacebookException, IOException {
      return this.marketplace_createListing(listingId, showOnProfile, listing.toString());
  }

  /**
   * Create a new marketplace listing.
   *
   * @param showOnProfile set to true to show the listing on the user's profile, set to false to prevent the listing from being shown on the profile.
   * @param listing the listing to publish.
   *
   * @return the id of the listing created (or modified).
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public Long marketplace_createListing(boolean showOnProfile, JSONObject listing) throws FacebookException, IOException {
      return this.marketplace_createListing(0l, showOnProfile, listing.toString());
  }

  /**
   * Return a list of all valid Marketplace categories.
   *
   * @return a list of marketplace categories allowed by Facebook.
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public List<String> marketplace_getCategories() throws FacebookException, IOException{
      this.callMethod(FacebookMethod.MARKET_GET_CATEGORIES);
      MarketplaceGetCategoriesResponse resp = (MarketplaceGetCategoriesResponse)this.getResponsePOJO();
      return resp.getMarketplaceCategory();
  }
  
  /**
   * Return a list of all valid Marketplace categories.
   *
   * @return a list of marketplace categories allowed by Facebook.
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public Document marketplace_getCategoriesObject() throws FacebookException, IOException{
      return this.callMethod(FacebookMethod.MARKET_GET_CATEGORIES);
  }

  /**
   * Return a list of all valid Marketplace subcategories.
   *
   * @return a list of marketplace subcategories allowed by Facebook.
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public List<String> marketplace_getSubCategories() throws FacebookException, IOException{
      this.callMethod(FacebookMethod.MARKET_GET_SUBCATEGORIES);
      MarketplaceGetSubCategoriesResponse resp = (MarketplaceGetSubCategoriesResponse)this.getResponsePOJO();
      return resp.getMarketplaceSubcategory();
  }

  /**
   * Retrieve listings from the marketplace.  The listings can be filtered by listing-id or user-id (or both).
   *
   * @param listingIds the ids of listings to filter by, only listings matching the specified ids will be returned.
   * @param uids the ids of users to filter by, only listings submitted by those users will be returned.
   *
   * @return A list of marketplace listings that meet the specified filter criteria.
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public List<Listing> marketplace_getListings(List<Long> listingIds, List<Long> uids) throws FacebookException, IOException {
      String listings = stringify(listingIds);
      String users = stringify(uids);

      Collection<Pair<String, CharSequence>> params = new ArrayList<Pair<String, CharSequence>>();
      if (listings != null) {
          params.add(new Pair<String, CharSequence>("listing_ids", listings));
      }
      if (uids != null) {
          params.add(new Pair<String, CharSequence>("uids", users));
      }

      this.callMethod(FacebookMethod.MARKET_GET_LISTINGS, params);
      MarketplaceGetListingsResponse resp = (MarketplaceGetListingsResponse)this.getResponsePOJO();
      return resp.getListing();
  }

  private String stringify(List input) {
      if ((input == null) || (input.isEmpty())) {
          return null;
      }
      String result = "";
      for (Object elem : input) {
          if (! "".equals(result)) {
              result += ",";
          }
          result += elem.toString();
      }
      return result;
  }

  /**
   * Search the marketplace listings by category, subcategory, and keyword.
   *
   * @param category the category to search in, optional (unless subcategory is specified).
   * @param subcategory the subcategory to search in, optional.
   * @param searchTerm the keyword to search for, optional.
   *
   * @return a list of marketplace entries that match the specified search parameters.
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public List<Listing> marketplace_search(MarketListingCategory category, MarketListingSubcategory subcategory, String searchTerm) throws FacebookException, IOException {
      if ("".equals(searchTerm)) {
          searchTerm = null;
      }
      if ((subcategory != null) && (category == null)) {
          throw new FacebookException(ErrorCode.GEN_INVALID_PARAMETER, "You cannot search by subcategory without also specifying a category!");
      }

      Collection<Pair<String, CharSequence>> params = new ArrayList<Pair<String, CharSequence>>();
      if (category != null) {
          params.add(new Pair<String, CharSequence>("category", category.getName()));
      }
      if (subcategory != null) {
          params.add(new Pair<String, CharSequence>("subcategory", subcategory.getName()));
      }
      if (searchTerm != null) {
          params.add(new Pair<String, CharSequence>("query", searchTerm));
      }

      this.callMethod(FacebookMethod.MARKET_SEARCH, params);
      MarketplaceSearchResponse resp = (MarketplaceSearchResponse)this.getResponsePOJO();
      return resp.getListing();
  }

  /**
   * Remove a listing from the marketplace by id.
   *
   * @param listingId the id of the listing to remove.
   * @param status the status to apply when removing the listing.  Should be one of MarketListingStatus.SUCCESS or MarketListingStatus.NOT_SUCCESS.
   *
   * @return true if the listing was successfully removed
   *         false otherwise
   *
   * @throws FacebookException if an error happens when executing the API call.
   * @throws IOException if a communication/network error happens.
   */
  public boolean marketplace_removeListing(Long listingId, MarketListingStatus status) throws FacebookException, IOException {
      if (status == null) {
          status = MarketListingStatus.DEFAULT;
      }
      if (listingId == null) {
          return false;
      }

      Collection<Pair<String, CharSequence>> params = new ArrayList<Pair<String, CharSequence>>();
      params.add(new Pair<String, CharSequence>("listing_id", listingId.toString()));
      params.add(new Pair<String, CharSequence>("status", status.getName()));
      this.callMethod(FacebookMethod.MARKET_REMOVE_LISTING, params);

      return this.rawResponse.contains(">1<"); //a code of '1' indicates success
  }
  
  /**
   * Clears the logged-in user's Facebook status.
   * Requires the status_update extended permission.
   * @return whether the status was successfully cleared
   * @see #users_hasAppPermission
   * @see FacebookExtendedPerm#STATUS_UPDATE
   * @see <a href="http://wiki.developers.facebook.com/index.php/Users.setStatus">
   *      Developers Wiki: Users.setStatus</a> 
   */
  public boolean users_clearStatus()
    throws FacebookException, IOException {
    return this.users_setStatus(null, true);
  }
  
  /**
   * Modify a marketplace listing
   * @param listingId identifies the listing to be modified
   * @param showOnProfile whether the listing can be shown on the user's profile
   * @param attrs the properties of the listing
   * @return the id of the edited listing
   * @see MarketplaceListing
   * @see <a href="http://wiki.developers.facebook.com/index.php/Marketplace.createListing">
   *      Developers Wiki: marketplace.createListing</a>
   *      
   * @deprecated provided for legacy support only.  Please use the version that takes a MarketListing instead.
   */
  public Long marketplace_editListing(Long listingId, Boolean showOnProfile, MarketplaceListing attrs)
    throws FacebookException, IOException {
    return this.marketplace_createListing(listingId, showOnProfile, attrs.getAttribs());
  }
  
  /**
   * Modify a marketplace listing
   * @param listingId identifies the listing to be modified
   * @param showOnProfile whether the listing can be shown on the user's profile
   * @param attrs the properties of the listing
   * @return the id of the edited listing
   * @see MarketplaceListing
   * @see <a href="http://wiki.developers.facebook.com/index.php/Marketplace.createListing">
   *      Developers Wiki: marketplace.createListing</a>
   */
  public Long marketplace_editListing(Long listingId, Boolean showOnProfile, MarketListing attrs)
    throws FacebookException, IOException {
    return this.marketplace_createListing(listingId, showOnProfile, attrs);
  }
  
  /**
   * Publish a story to the logged-in user's newsfeed.
   * @param title the title of the feed story
   * @param body the body of the feed story
   * @param images (optional) up to four pairs of image URLs and (possibly null) link URLs
   * @return whether the story was successfully published; false in case of permission error
   * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishStoryToUser">
   *      Developers Wiki: Feed.publishStoryToUser</a>
   */
  public boolean feed_publishStoryToUser(CharSequence title, CharSequence body,
                                         Collection<? extends Pair<URL, URL>> images)
    throws FacebookException, IOException {
    return feed_publishStoryToUser(title, body, images, null);
  }
  
  /**
   * Create a marketplace listing
   * @param showOnProfile whether the listing can be shown on the user's profile
   * @param attrs the properties of the listing
   * @return the id of the created listing
   * @see MarketplaceListing
   * @see <a href="http://wiki.developers.facebook.com/index.php/Marketplace.createListing">
   *      Developers Wiki: marketplace.createListing</a>
   *      
   * @deprecated provided for legacy support only.
   */
  public Long marketplace_createListing(Boolean showOnProfile, MarketplaceListing attrs)
    throws FacebookException, IOException {
    return this.marketplace_createListing(null, showOnProfile, attrs.getAttribs());
  }

    /* (non-Javadoc)
     * @see com.facebook.api.IFacebookRestClient#auth_getUserId(java.lang.String)
     */
    public long auth_getUserId(String authToken) throws FacebookException, IOException {
        if (null == this._sessionKey)
            auth_getSession(authToken);
        return this.users_getLoggedInUser();
    }
    
    /* (non-Javadoc)
     * @see com.facebook.api.IFacebookRestClient#feed_publishActionOfUser(java.lang.CharSequence, java.lang.CharSequence, java.util.Collection)
     */
    public boolean feed_publishActionOfUser(CharSequence title, CharSequence body, Collection<? extends Pair<URL, URL>> images) throws FacebookException, IOException {
        return this.feed_publishActionOfUser(title, body, images, null);
    }
    
    /* (non-Javadoc)
     * @see com.facebook.api.IFacebookRestClient#feed_publishTemplatizedAction(java.lang.Long, java.lang.CharSequence)
     */
    public boolean feed_publishTemplatizedAction(Long actorId, CharSequence titleTemplate) throws FacebookException, IOException {
        return this.feed_publishTemplatizedAction(titleTemplate.toString(), null, null, null, null, null, null);
    }
    
    /* (non-Javadoc)
     * @see com.facebook.api.IFacebookRestClient#feed_publishTemplatizedAction(java.lang.Long, java.lang.CharSequence, java.util.Map, java.lang.CharSequence, java.util.Map, java.lang.CharSequence, java.util.Collection, java.util.Collection)
     */
    public boolean feed_publishTemplatizedAction(Long actorId, CharSequence titleTemplate, Map<String,CharSequence> titleData, CharSequence bodyTemplate, Map<String,CharSequence> bodyData, CharSequence bodyGeneral, Collection<Long> targetIds, Collection<? extends Pair<URL, URL>> images) throws FacebookException, IOException {
        return this.feed_publishTemplatizedAction(titleTemplate.toString(), 
                titleData.toString(), bodyTemplate.toString(), bodyData.toString(), bodyGeneral.toString(), images, targetIds.toString());
    }
    
    /** 
     * @deprecated provided for legacy support only.  Use the version that returns a List<String> instead.
     */
    public Document marketplace_getListings(Collection<Long> listingIds, Collection<Long> userIds) throws FacebookException, IOException {
        ArrayList<Pair<String, CharSequence>> params =
            new ArrayList<Pair<String, CharSequence>>(FacebookMethod.MARKETPLACE_GET_LISTINGS.numParams());
        if (null != listingIds && !listingIds.isEmpty()) {
            params.add(new Pair<String, CharSequence>("listing_ids", delimit(listingIds)));
        }
        if (null != userIds && !userIds.isEmpty()) {
            params.add(new Pair<String, CharSequence>("uids", delimit(userIds)));
        }

        assert !params.isEmpty() : "Either listingIds or userIds should be provided";
        return this.callMethod(FacebookMethod.MARKETPLACE_GET_LISTINGS, params);
    }
    
    /* (non-Javadoc)
     * @see com.facebook.api.IFacebookRestClient#marketplace_getSubCategories(java.lang.CharSequence)
     */
    public Document marketplace_getSubCategories(CharSequence category) throws FacebookException, IOException {
        if (category != null) {
            return this.callMethod(FacebookMethod.MARKET_GET_SUBCATEGORIES, new Pair<String, CharSequence>("category", category));
        }
        return this.callMethod(FacebookMethod.MARKET_GET_SUBCATEGORIES);
    }
    
    /* (non-Javadoc)
     * @see com.facebook.api.IFacebookRestClient#marketplace_removeListing(java.lang.Long)
     */
    public boolean marketplace_removeListing(Long listingId) throws FacebookException, IOException {
        return this.marketplace_removeListing(listingId, MarketListingStatus.DEFAULT);
    }
    
    /**
     * @deprecated provided for legacy support only.  Use marketplace_removeListing(Long, MarketListingStatus) instead.
     */
    public boolean marketplace_removeListing(Long listingId, CharSequence status) throws FacebookException, IOException {
        return this.marketplace_removeListing(listingId);
    }
    
    /** 
     * @deprecated provided for legacy support only.  Use the version that returns a List<Listing> instead.
     */
    public Document marketplace_search(CharSequence category, CharSequence subCategory, CharSequence query) throws FacebookException, IOException {
        if ("".equals(query)) {
            query = null;
        }
        if ((subCategory != null) && (category == null)) {
            throw new FacebookException(ErrorCode.GEN_INVALID_PARAMETER, "You cannot search by subcategory without also specifying a category!");
        }

        Collection<Pair<String, CharSequence>> params = new ArrayList<Pair<String, CharSequence>>();
        if (category != null) {
            params.add(new Pair<String, CharSequence>("category", category));
        }
        if (subCategory != null) {
            params.add(new Pair<String, CharSequence>("subcategory", subCategory));
        }
        if (query != null) {
            params.add(new Pair<String, CharSequence>("query", query));
        }

        return this.callMethod(FacebookMethod.MARKET_SEARCH, params);
    }
    
    /**
     * @deprecated provided for legacy support only.  Use users_hasAppPermission(Permission) instead.
     */
    public boolean users_hasAppPermission(CharSequence permission) throws FacebookException, IOException {
        this.callMethod(FacebookMethod.USERS_HAS_PERMISSION, new Pair<String, CharSequence>("ext_perm", permission));
        return this.rawResponse.contains(">1<");  //a code of '1' is sent back to indicate that the user has the request permission
    }
    
    /* (non-Javadoc)
     * @see com.facebook.api.IFacebookRestClient#users_setStatus(java.lang.String)
     */
    public boolean users_setStatus(String status) throws FacebookException, IOException {
        return this.users_setStatus(status, false);
    }
    
    /**
     * Used to retrieve photo objects using the search parameters (one or more of the
     * parameters must be provided).
     *
     * @param albumId retrieve from photos from this album (optional)
     * @param photoIds retrieve from this list of photos (optional)
     * @return an T of photo objects.
     * @see #photos_get(Integer, Long, Collection)
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.get">
     *      Developers Wiki: Photos.get</a> 
     */
    public Document photos_getByAlbum(Long albumId, Collection<Long> photoIds)
      throws FacebookException, IOException {
      return photos_get(null /*subjId*/, albumId, photoIds);
    }
    
    /**
     * Used to retrieve photo objects using the search parameters (one or more of the
     * parameters must be provided).
     *
     * @param albumId retrieve from photos from this album (optional)
     * @return an T of photo objects.
     * @see #photos_get(Integer, Long, Collection)
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.get">
     *      Developers Wiki: Photos.get</a> 
     */
    public Document photos_getByAlbum(Long albumId)
      throws FacebookException, IOException {
      return photos_get(null /*subjId*/, albumId, null /*photoIds*/);
    }
}