All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.code.facebookapi.BasicClient Maven / Gradle / Ivy

There is a newer version: 3.0.4
Show newest version
package com.google.code.facebookapi;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONArray;

/**
 * Basic client taking care of rest call mechanics (signing, etc) to facebook. No api knowledge, nor response interpretation is planned.
 */
public class BasicClient {

	protected static Log log = LogFactory.getLog( BasicClient.class );

	protected static final String CRLF = "\r\n";
	protected static final String PREF = "--";
	protected static final int UPLOAD_BUFFER_SIZE = 1024;

	protected URL _serverUrl;
	protected int _timeout;
	protected int _readTimeout;

	protected final String _apiKey;
	protected final String _secret;
	protected boolean _isDesktop;

	protected String cacheSessionKey;
	protected Long cacheSessionExpires;

	/** filled in when session is established only used for desktop apps */
	protected String cacheSessionSecret;

	protected String rawResponse;
	protected boolean batchMode;

	public boolean isBatchMode() {
		return batchMode;
	}

	protected List queries;

	public List getQueries() {
		return queries;
	}

	protected String permissionsApiKey = null;


	public boolean isDesktop() {
		return _isDesktop;
	}

	public String getRawResponse() {
		return rawResponse;
	}





	protected BasicClient( String apiKey, String secret ) {
		this( null, apiKey, secret, null );
	}

	protected BasicClient( String apiKey, String secret, int timeout ) {
		this( null, apiKey, secret, null, timeout );
	}

	protected BasicClient( String apiKey, String secret, String sessionKey ) {
		this( null, apiKey, secret, sessionKey );
	}

	protected BasicClient( String apiKey, String secret, String sessionKey, int timeout ) {
		this( null, apiKey, secret, sessionKey, timeout );
	}

	protected BasicClient( URL serverUrl, String apiKey, String secret, String sessionKey, int timeout ) {
		this( serverUrl, apiKey, secret, sessionKey );
		_timeout = timeout;
	}

	protected BasicClient( URL serverUrl, String apiKey, String secret, String sessionKey, int timeout, int readTimeout ) {
		this( serverUrl, apiKey, secret, sessionKey );
		_timeout = timeout;
		_readTimeout = readTimeout;
	}

	protected BasicClient( URL serverUrl, String apiKey, String secret, String sessionKey ) {
		this.cacheSessionKey = sessionKey;
		this._apiKey = apiKey;
		this._secret = secret;
		if ( secret.endsWith( "__" ) ) {
			_isDesktop = true;
		}
		this._serverUrl = ( null != serverUrl ) ? serverUrl : FacebookApiUrls.getDefaultServerUrl();
		this._timeout = -1;
		this._readTimeout = -1;
		this.batchMode = false;
		this.queries = new ArrayList();
	}

	public String getApiKey() {
		return _apiKey;
	}

	public String getSecret() {
		return _secret;
	}

	public void beginPermissionsMode( String apiKey ) {
		this.permissionsApiKey = apiKey;
	}

	public void endPermissionsMode() {
		this.permissionsApiKey = null;
	}

	/**
	 * Gets the session-token used by Facebook to authenticate a desktop application. If your application does not run in desktop mode, than this field is not relevent to
	 * you.
	 * 
	 * @return the desktop-app session token.
	 */
	public String getSessionSecret() {
		return cacheSessionSecret;
	}

	/**
	 * Allows the session-token to be manually overridden when running a desktop application. If your application does not run in desktop mode, then setting this field
	 * will have no effect. If you set an incorrect value here, your application will probably fail to run.
	 * 
	 * @param key
	 *            the new value to set. Incorrect values may cause your application to fail to run.
	 */
	public void setSessionSecret( String key ) {
		cacheSessionSecret = key;
	}

	public String getCacheSessionSecret() {
		return cacheSessionSecret;
	}

	public void setCacheSessionSecret( String cacheSessionSecret ) {
		this.cacheSessionSecret = cacheSessionSecret;
	}

	public Long getCacheSessionExpires() {
		return cacheSessionExpires;
	}

	public void setCacheSessionExpires( Long cacheSessionExpires ) {
		this.cacheSessionExpires = cacheSessionExpires;
	}

	public String getCacheSessionKey() {
		return cacheSessionKey;
	}

	public void setCacheSessionKey( String cacheSessionKey ) {
		this.cacheSessionKey = cacheSessionKey;
	}

	/**
	 * 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 String callMethod( String responseFormat, IFacebookMethod method, Pair... paramPairs ) throws FacebookException {
		return callMethod( responseFormat, method, Arrays.asList( paramPairs ), null, null );
	}

	/**
	 * 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.
	 */
	public String callMethod( String responseFormat, IFacebookMethod method, Collection> paramPairs ) throws FacebookException {
		return callMethod( responseFormat, method, paramPairs, null, null );
	}

	public String callMethod( String responseFormat, IFacebookMethod method, Collection> paramPairs, String fileName, InputStream fileStream )
			throws FacebookException {
		rawResponse = null;

		Map params = new TreeMap();

		if ( permissionsApiKey != null ) {
			params.put( "call_as_apikey", permissionsApiKey );
		}

		params.put( "method", method.methodName() );
		params.put( "api_key", _apiKey );
		params.put( "v", IFacebookRestClient.TARGET_API_VERSION );

		if ( null != responseFormat ) {
			params.put( "format", responseFormat );
		}

		params.put( "call_id", Long.toString( System.currentTimeMillis() ) );
		boolean includeSession = !method.requiresNoSession() && cacheSessionKey != null;
		if ( includeSession ) {
			params.put( "session_key", cacheSessionKey );
		}

		CharSequence oldVal;
		for ( Pair p : paramPairs ) {
			oldVal = params.put( p.first, FacebookSignatureUtil.toString( p.second ) );
			if ( oldVal != null ) {
				log.warn( String.format( "For parameter %s, overwrote old value %s with new value %s.", p.first, oldVal, p.second ) );
			}
		}

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

		if ( batchMode ) {
			// if we are running in batch mode, don't actually execute the query now, just add it to the list
			boolean addToBatch = true;
			if ( method.methodName().equals( FacebookMethod.USERS_GET_LOGGED_IN_USER.methodName() ) ) {
				Exception trace = new Exception();
				StackTraceElement[] traceElems = trace.getStackTrace();
				int index = 0;
				for ( StackTraceElement elem : traceElems ) {
					if ( elem.getMethodName().indexOf( "_" ) != -1 ) {
						StackTraceElement caller = traceElems[index + 1];
						if ( ( caller.getClassName().equals( BasicClient.class.getName() ) ) && ( !caller.getMethodName().startsWith( "auth_" ) ) ) {
							addToBatch = false;
						}
						break;
					}
					index++ ;
				}
			}
			if ( addToBatch ) {
				queries.add( new BatchQuery( method, params ) );
			}
			return null;
		}

		boolean doHttps = FacebookMethod.AUTH_GET_SESSION.equals( method ) && "true".equals( params.get( "generate_session_secret" ) );
		try {
			rawResponse = method.takesFile() ? postFileRequest( method, params, fileName, fileStream ) : postRequest( method, params, doHttps );
			return rawResponse;
		}
		catch ( IOException ex ) {
			throw BasicClientHelper.runtimeException( ex );
		}
	}

	private String postRequest( IFacebookMethod method, Map params, boolean doHttps ) throws IOException {
		URL serverUrl = ( doHttps ) ? FacebookApiUrls.getDefaultHttpsServerUrl() : _serverUrl;
		CharSequence paramString = ( null == params ) ? "" : BasicClientHelper.delimit( params.entrySet(), "&", "=", true );
		if ( log.isDebugEnabled() ) {
			log.debug( method.methodName() + " POST: " + serverUrl.toString() + "?" + paramString );
		}

		HttpURLConnection conn = null;
		OutputStream out = null;
		InputStream in = null;
		try {
			conn = (HttpURLConnection) serverUrl.openConnection();
			if ( _timeout != -1 ) {
				conn.setConnectTimeout( _timeout );
			}
			if ( _readTimeout != -1 ) {
				conn.setReadTimeout( _readTimeout );
			}
			conn.setRequestMethod( "POST" );
			conn.setDoOutput( true );
			conn.connect();
			out = conn.getOutputStream();
			out.write( paramString.toString().getBytes( "UTF-8" ) );
			in = conn.getInputStream();
			return BasicClientHelper.getResponse( method, in );
		}
		finally {
			BasicClientHelper.close( in );
			BasicClientHelper.close( out );
			BasicClientHelper.disconnect( conn );
		}
	}

	/**
	 * Helper function for posting a request that includes raw file data, eg {@link #photos_upload}.
	 * 
	 * @param methodName
	 *            the name of the method
	 * @param params
	 *            request parameters (not including the file)
	 * @param doEncode
	 *            whether to UTF8-encode the parameters
	 * @return an InputStream with the request response
	 * @see #photos_upload
	 */

	protected String postFileRequest( IFacebookMethod method, Map params, String fileName, InputStream fileStream ) throws IOException {
		HttpURLConnection con = null;
		OutputStream urlOut = null;
		InputStream in = null;
		try {
			String boundary = Long.toString( System.currentTimeMillis(), 16 );
			con = (HttpURLConnection) _serverUrl.openConnection();
			if ( _timeout != -1 ) {
				con.setConnectTimeout( _timeout );
			}
			if ( _readTimeout != -1 ) {
				con.setReadTimeout( _readTimeout );
			}
			con.setDoInput( true );
			con.setDoOutput( true );
			con.setUseCaches( false );
			con.setRequestProperty( "Content-Type", "multipart/form-data; boundary=" + boundary );
			con.setRequestProperty( "MIME-version", "1.0" );

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

			for ( Map.Entry entry : params.entrySet() ) {
				out.writeBytes( PREF + boundary + CRLF );

				out.writeBytes( "Content-Type: text/plain;charset=utf-8" + CRLF );
				// out.writeBytes( "Content-Transfer-Encoding: application/x-www-form-urlencoded" + CRLF );

				// out.writeBytes( "Content-Type: text/plain;charset=utf-8" + CRLF );
				// out.writeBytes( "Content-Transfer-Encoding: quoted-printable" + CRLF );

				out.writeBytes( "Content-disposition: form-data; name=\"" + entry.getKey() + "\"" + CRLF );
				out.writeBytes( CRLF );
				byte[] valueBytes = entry.getValue().toString().getBytes( "UTF-8" );
				out.write( valueBytes );
				out.writeBytes( CRLF );
			}

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

			// Write the file
			out.writeBytes( CRLF );
			byte buf[] = new byte[UPLOAD_BUFFER_SIZE];
			int len = 0;
			while ( len >= 0 ) {
				out.write( buf, 0, len );
				len = fileStream.read( buf );
			}

			out.writeBytes( CRLF + PREF + boundary + PREF + CRLF );
			out.flush();
			in = con.getInputStream();
			return BasicClientHelper.getResponse( method, in );
		}
		finally {
			BasicClientHelper.close( urlOut );
			BasicClientHelper.close( in );
			BasicClientHelper.disconnect( con );
		}
	}

	public void beginBatch() {
		batchMode = true;
		queries = new ArrayList();
	}

	@SuppressWarnings("unchecked")
	public String batch_run( String responseFormat, String methods, boolean serial ) throws FacebookException {
		if ( !serial ) {
			return callMethod( responseFormat, FacebookMethod.BATCH_RUN, Pairs.newPair( "method_feed", methods ) );
		} else {
			return callMethod( responseFormat, FacebookMethod.BATCH_RUN, Pairs.newPair( "method_feed", methods ), Pairs.newPair( "serial_only", "1" ) );
		}
	}

	public void setServerUrl( String newUrl ) {
		String base = newUrl;
		if ( base.startsWith( "http" ) ) {
			base = base.substring( base.indexOf( "://" ) + 3 );
		}
		try {
			String url = "http://" + base;
			_serverUrl = new URL( url );
			// do not set default url
			// setDefaultServerUrl( _serverUrl );
		}
		catch ( MalformedURLException ex ) {
			throw BasicClientHelper.runtimeException( ex );
		}
	}

	/**
	 * Returns a list of String raw responses which will be further broken down by the adapters into the actual individual responses. One string is returned per 20
	 * methods in the batch.
	 */
	public List executeBatch( String responseFormat, boolean serial ) throws FacebookException {
		int BATCH_LIMIT = 20;
		this.batchMode = false;
		List result = new ArrayList();
		List buffer = new ArrayList();

		while ( !this.queries.isEmpty() ) {
			buffer.add( this.queries.remove( 0 ) );
			if ( ( buffer.size() == BATCH_LIMIT ) || ( this.queries.isEmpty() ) ) {
				// we can only actually batch up to 20 at once

				String batchRawResponse = batch_run( responseFormat, encodeMethods( buffer ), serial );
				result.add( batchRawResponse );

				if ( buffer.size() == BATCH_LIMIT ) {
					log.debug( "Clearing buffer for the next run." );
					buffer.clear();
				} else {
					log.trace( "No need to clear buffer, this is the final iteration of the batch" );
				}
			}
		}

		return result;
	}

	public static String encodeMethods( List queryList ) throws FacebookException {
		JSONArray result = new JSONArray();
		for ( BatchQuery query : queryList ) {
			if ( query.getMethod().takesFile() ) {
				throw new FacebookException( ErrorCode.GEN_INVALID_PARAMETER, "File upload API calls cannot be batched:  " + query.getMethod().methodName() );
			}
			result.put( BasicClientHelper.delimit( query.getParams().entrySet(), "&", "=", true ) );
		}
		return result.toString();
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy