001    /*
002     * Copyright 2003-2008 the original author or authors.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *     http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     *
016     * You are receiving this code free of charge, which represents many hours of
017     * effort from other individuals and corporations.  As a responsible member 
018     * of the community, you are asked (but not required) to donate any 
019     * enhancements or improvements back to the community under a similar open 
020     * source license.  Thank you. -TMN
021     */
022    package groovyx.net.http;
023    
024    import java.io.IOException;
025    import java.net.URISyntaxException;
026    import java.util.Map;
027    import java.util.concurrent.Callable;
028    import java.util.concurrent.Executors;
029    import java.util.concurrent.Future;
030    import java.util.concurrent.ThreadPoolExecutor;
031    
032    import org.apache.http.HttpVersion;
033    import org.apache.http.client.ClientProtocolException;
034    import org.apache.http.conn.ClientConnectionManager;
035    import org.apache.http.conn.params.ConnManagerParams;
036    import org.apache.http.conn.params.ConnPerRouteBean;
037    import org.apache.http.conn.scheme.PlainSocketFactory;
038    import org.apache.http.conn.scheme.Scheme;
039    import org.apache.http.conn.scheme.SchemeRegistry;
040    import org.apache.http.conn.ssl.SSLSocketFactory;
041    import org.apache.http.impl.client.DefaultHttpClient;
042    import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
043    import org.apache.http.params.BasicHttpParams;
044    import org.apache.http.params.HttpParams;
045    import org.apache.http.params.HttpProtocolParams;
046    
047    /**
048     * This implementation makes all requests asynchronous by submitting jobs to a 
049     * {@link ThreadPoolExecutor}.  All request methods (including <code>get</code> 
050     * and <code>post</code>) return a {@link Future} instance, whose 
051     * {@link Future#get() get} method will provide access to whatever value was 
052     * returned from the response handler closure.
053     *  
054     * @author <a href='mailto:tomstrummer+httpbuilder@gmail.com'>Tom Nichols</a>
055     */
056    public class AsyncHTTPBuilder extends HTTPBuilder {
057    
058            /**
059             * Default pool size is one is not supplied in the constructor.
060             */
061            public static final int DEFAULT_POOL_SIZE = 4;
062            
063            protected final ThreadPoolExecutor threadPool = 
064                    (ThreadPoolExecutor)Executors.newCachedThreadPool();
065            
066            /**
067             * Accepts the following named parameters:
068             * <dl>
069             *  <dt>poolSize</dt><dd>Max number of concurrent requests</dd>
070             *  <dt>url</dt><dd>Default request URL</dd>
071             *  <dt>contentType</dt><dd>Default content type for requests and responses</dd>
072             * </dl>
073             */
074            public AsyncHTTPBuilder( Map<String, ?> args ) throws URISyntaxException {
075                    super();
076                    Object poolSize = args.get("poolSize");
077                    if ( poolSize == null ) poolSize = DEFAULT_POOL_SIZE;
078                    this.initThreadPools( (Integer)poolSize );
079                    
080                    Object defaultURL = args.get("url");
081                    if ( defaultURL != null ) super.setURL(defaultURL);
082    
083                    Object defaultContentType = args.get("contentType");
084                    if ( defaultContentType != null ) 
085                            super.setContentType(defaultContentType);
086            }
087            
088            /**
089             * Submits a {@link Callable} instance to the job pool, which in turn will 
090             * call {@link HTTPBuilder#doRequest(SendDelegate)} in an asynchronous 
091             * thread.  The {@link Future} instance returned by this value (which in 
092             * turn should be returned by any of the public <code>request</code> methods
093             * (including <code>get</code> and <code>post</code>) may be used to 
094             * retrieve whatever value may be returned from the executed response 
095             * handler closure. 
096             */
097            @Override
098            protected Future<?> doRequest( final SendDelegate delegate ) {
099                    return threadPool.submit( new Callable<Object>() {
100                            /*@Override*/ public Object call() throws Exception {
101                                    try {
102                                            return doRequestSuper(delegate);
103                                    }
104                                    catch( Exception ex ) {
105                                            log.error( "Exception thrown from request delegate: " + 
106                                                            delegate, ex );
107                                            throw ex;
108                                    }
109                            }
110                    });
111            }
112            
113            /*
114             * Because we can't call "super.doRequest" from within the anonymous 
115             * Callable subclass.
116             */
117            private Object doRequestSuper( SendDelegate delegate ) throws IOException {
118                    return super.doRequest(delegate);
119            }
120            
121            /**
122             * Initializes threading parameters for the HTTPClient's 
123             * {@link ThreadSafeClientConnManager}, and this class' ThreadPoolExecutor. 
124             */
125            protected void initThreadPools( final int poolSize ) {
126                    if (poolSize < 1) throw new IllegalArgumentException("poolSize may not be < 1");
127                    // Create and initialize HTTP parameters
128                    HttpParams params = client != null ? client.getParams()
129                                    : new BasicHttpParams();
130                    ConnManagerParams.setMaxTotalConnections(params, poolSize);
131                    ConnManagerParams.setMaxConnectionsPerRoute(params,
132                                    new ConnPerRouteBean(poolSize));
133    
134                    HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
135    
136                    // Create and initialize scheme registry
137                    SchemeRegistry schemeRegistry = new SchemeRegistry();
138                    schemeRegistry.register( new Scheme( "http", 
139                                    PlainSocketFactory.getSocketFactory(), 80 ) );
140                    schemeRegistry.register( new Scheme( "https", 
141                                    SSLSocketFactory.getSocketFactory(), 443));
142    
143                    ClientConnectionManager cm = new ThreadSafeClientConnManager(
144                                    params, schemeRegistry );
145                    super.client = new DefaultHttpClient( cm, params );
146    
147                    /* Although the thread pool is flexible, it cannot become bigger than
148                     * the max size of the connection pool-- otherwise threads will be
149                     * created in this pool for new jobs, but they will all block when
150                     * waiting for a free connection to send the request.
151                     */
152                    this.threadPool.setMaximumPoolSize(poolSize);
153            }
154            
155            /**
156             * <p>Access the underlying threadpool to adjust things like job timeouts.</p>  
157             * 
158             * <p>Note that this is not the same thread pool used by the HttpClient's 
159             * {@link ThreadSafeClientConnManager}.  Therefore, increasing the 
160             * {@link ThreadPoolExecutor#setMaximumPoolSize(int) maximum pool size} will
161             * not in turn increase the number of possible concurrent requests.  It will
162             * simply cause more requests to be <i>attempted</i> which will then simply
163             * block while waiting for an available connection.</p>
164             * 
165             * <p>Since {@link ThreadSafeClientConnManager} has no public mechanism to 
166             * adjust its pool size, the value 
167             * @return
168             */
169            public ThreadPoolExecutor getThreadPoolExecutor() {
170                    return this.threadPool;
171            }
172            
173            /**
174             * {@inheritDoc}
175             */
176            @Override public void shutdown() {
177                    super.shutdown(); 
178                    this.threadPool.shutdown();
179            }
180            
181            /**
182             * {@inheritDoc}
183             * @see #shutdown()
184             */
185            @Override protected void finalize() throws Throwable {
186                    this.shutdown();
187                    super.finalize();
188            }
189    }