/* * Copyright 2008-2011 Thomas Nichols. http://blog.thomnichols.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * You are receiving this code free of charge, which represents many hours of * effort from other individuals and corporations. As a responsible member * of the community, you are encouraged (but not required) to donate any * enhancements or improvements back to the community under a similar open * source license. Thank you. -TMN */ package groovyx.net.http; import java.io.IOException; import java.net.URISyntaxException; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.http.HttpVersion; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.params.ConnManagerParams; import org.apache.http.conn.params.ConnPerRouteBean; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; /** * This implementation makes all requests asynchronous by submitting jobs to a * {@link ThreadPoolExecutor}. All request methods (including <code>get</code> * and <code>post</code>) return a {@link Future} instance, whose * {@link Future#get() get} method will provide access to whatever value was * returned from the response handler closure. * * @author <a href='mailto:tomstrummer+httpbuilder@gmail.com'>Tom Nichols</a> */ public class AsyncHTTPBuilder extends HTTPBuilder { /** * Default pool size is one is not supplied in the constructor. */ public static final int DEFAULT_POOL_SIZE = 4; protected ExecutorService threadPool; // = (ThreadPoolExecutor)Executors.newCachedThreadPool(); /** * Accepts the following named parameters: * <dl> * <dt>threadPool</dt><dd>Custom {@link ExecutorService} instance for * running submitted requests. If this is an instance of {@link ThreadPoolExecutor}, * the poolSize will be determined by {@link ThreadPoolExecutor#getMaximumPoolSize()}. * The default threadPool uses an unbounded queue to accept an unlimited * number of requests.</dd> * <dt>poolSize</dt><dd>Max number of concurrent requests</dd> * <dt>uri</dt><dd>Default request URI</dd> * <dt>contentType</dt><dd>Default content type for requests and responses</dd> * <dt>timeout</dt><dd>Timeout in milliseconds to wait for a connection to * be established and request to complete.</dd> * </dl> */ public AsyncHTTPBuilder( Map<String, ?> args ) throws URISyntaxException { int poolSize = DEFAULT_POOL_SIZE; ExecutorService threadPool = null; if ( args != null ) { threadPool = (ExecutorService)args.remove( "threadPool" ); if ( threadPool instanceof ThreadPoolExecutor ) poolSize = ((ThreadPoolExecutor)threadPool).getMaximumPoolSize(); Object poolSzArg = args.remove("poolSize"); if ( poolSzArg != null ) poolSize = Integer.parseInt( poolSzArg.toString() ); if ( args.containsKey( "url" ) ) throw new IllegalArgumentException( "The 'url' parameter is deprecated; use 'uri' instead" ); Object defaultURI = args.remove("uri"); if ( defaultURI != null ) super.setUri(defaultURI); Object defaultContentType = args.remove("contentType"); if ( defaultContentType != null ) super.setContentType(defaultContentType); Object timeout = args.remove( "timeout" ); if ( timeout != null ) setTimeout( (Integer) timeout ); if ( args.size() > 0 ) { String invalidArgs = ""; for ( String k : args.keySet() ) invalidArgs += k + ","; throw new IllegalArgumentException("Unexpected keyword args: " + invalidArgs); } } this.initThreadPools( poolSize, threadPool ); } /** * Submits a {@link Callable} instance to the job pool, which in turn will * call {@link HTTPBuilder#doRequest(RequestConfigDelegate)} in an asynchronous * thread. The {@link Future} instance returned by this value (which in * turn should be returned by any of the public <code>request</code> methods * (including <code>get</code> and <code>post</code>) may be used to * retrieve whatever value may be returned from the executed response * handler closure. */ @Override protected Future<?> doRequest( final RequestConfigDelegate delegate ) { return threadPool.submit( new Callable<Object>() { /*@Override*/ public Object call() throws Exception { try { return doRequestSuper(delegate); } catch( Exception ex ) { log.info( "Exception thrown from response delegate: " + delegate, ex ); throw ex; } } }); } /* * Because we can't call "super.doRequest" from within the anonymous * Callable subclass. */ private Object doRequestSuper( RequestConfigDelegate delegate ) throws IOException { return super.doRequest(delegate); } /** * Initializes threading parameters for the HTTPClient's * {@link ThreadSafeClientConnManager}, and this class' ThreadPoolExecutor. */ protected void initThreadPools( final int poolSize, final ExecutorService threadPool ) { if (poolSize < 1) throw new IllegalArgumentException("poolSize may not be < 1"); // Create and initialize HTTP parameters HttpParams params = super.getClient().getParams(); ConnManagerParams.setMaxTotalConnections(params, poolSize); ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(poolSize)); HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); // Create and initialize scheme registry SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register( new Scheme( "http", PlainSocketFactory.getSocketFactory(), 80 ) ); schemeRegistry.register( new Scheme( "https", SSLSocketFactory.getSocketFactory(), 443)); ClientConnectionManager cm = new ThreadSafeClientConnManager( params, schemeRegistry ); setClient(new DefaultHttpClient( cm, params )); this.threadPool = threadPool != null ? threadPool : new ThreadPoolExecutor( poolSize, poolSize, 120, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>() ); } /** * {@inheritDoc} */ @Override protected Object defaultSuccessHandler( HttpResponseDecorator resp, Object parsedData ) throws ResponseParseException { return super.defaultSuccessHandler( resp, parsedData ); } /** * For 'failure' responses (e.g. a 404), the exception will be wrapped in * a {@link ExecutionException} and held by the {@link Future} instance. * The exception is then re-thrown when calling {@link Future#get() * future.get()}. You can access the original exception (e.g. an * {@link HttpResponseException}) by calling <code>ex.getCause()</code>. * */ @Override protected void defaultFailureHandler( HttpResponseDecorator resp ) throws HttpResponseException { super.defaultFailureHandler( resp ); } /** * This timeout is used for both the time to wait for an established * connection, and the time to wait for data. * @see HttpConnectionParams#setSoTimeout(HttpParams, int) * @see HttpConnectionParams#setConnectionTimeout(HttpParams, int) * @param timeout time to wait in milliseconds. */ public void setTimeout( int timeout ) { HttpConnectionParams.setConnectionTimeout( super.getClient().getParams(), timeout ); HttpConnectionParams.setSoTimeout( super.getClient().getParams(), timeout ); /* this will cause a thread waiting for an available connection instance * to time-out */ // ConnManagerParams.setTimeout( super.getClient().getParams(), timeout ); } /** * Get the timeout in for establishing an HTTP connection. * @return timeout in milliseconds. */ public int getTimeout() { return HttpConnectionParams.getConnectionTimeout( super.getClient().getParams() ); } /** * <p>Access the underlying threadpool to adjust things like job timeouts.</p> * * <p>Note that this is not the same pool used by the HttpClient's * {@link ThreadSafeClientConnManager}. Therefore, increasing the * {@link ThreadPoolExecutor#setMaximumPoolSize(int) maximum pool size} will * not in turn increase the number of possible concurrent requests. It will * simply cause more requests to be <i>attempted</i> which will then simply * block while waiting for a free connection.</p> * * @return the service used to execute requests. By default this is a * {@link ThreadPoolExecutor}. */ public ExecutorService getThreadExecutor() { return this.threadPool; } /** * {@inheritDoc} */ @Override public void shutdown() { super.shutdown(); this.threadPool.shutdown(); } /** * {@inheritDoc} * @see #shutdown() */ @Override protected void finalize() throws Throwable { this.shutdown(); super.finalize(); } }