001/*
002 * Copyright 2008-2011 Thomas Nichols.  http://blog.thomnichols.org
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 encouraged (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 */
022package groovyx.net.http;
023
024import java.io.IOException;
025import java.net.URISyntaxException;
026import java.util.Map;
027import java.util.concurrent.Callable;
028import java.util.concurrent.ExecutionException;
029import java.util.concurrent.ExecutorService;
030import java.util.concurrent.Future;
031import java.util.concurrent.LinkedBlockingQueue;
032import java.util.concurrent.ThreadPoolExecutor;
033import java.util.concurrent.TimeUnit;
034
035import org.apache.http.HttpVersion;
036import org.apache.http.conn.ClientConnectionManager;
037import org.apache.http.conn.params.ConnManagerParams;
038import org.apache.http.conn.params.ConnPerRouteBean;
039import org.apache.http.conn.scheme.PlainSocketFactory;
040import org.apache.http.conn.scheme.Scheme;
041import org.apache.http.conn.scheme.SchemeRegistry;
042import org.apache.http.conn.ssl.SSLSocketFactory;
043import org.apache.http.impl.client.DefaultHttpClient;
044import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
045import org.apache.http.params.BasicHttpParams;
046import org.apache.http.params.HttpConnectionParams;
047import org.apache.http.params.HttpParams;
048import org.apache.http.params.HttpProtocolParams;
049
050/**
051 * This implementation makes all requests asynchronous by submitting jobs to a
052 * {@link ThreadPoolExecutor}.  All request methods (including <code>get</code>
053 * and <code>post</code>) return a {@link Future} instance, whose
054 * {@link Future#get() get} method will provide access to whatever value was
055 * returned from the response handler closure.
056 *
057 * @author <a href='mailto:[email protected]'>Tom Nichols</a>
058 */
059public class AsyncHTTPBuilder extends HTTPBuilder {
060
061    /**
062     * Default pool size is one is not supplied in the constructor.
063     */
064    public static final int DEFAULT_POOL_SIZE = 4;
065
066    protected ExecutorService threadPool;
067//      = (ThreadPoolExecutor)Executors.newCachedThreadPool();
068
069    /**
070     * Accepts the following named parameters:
071     * <dl>
072     *  <dt>threadPool</dt><dd>Custom {@link ExecutorService} instance for
073     *      running submitted requests.  If this is an instance of {@link ThreadPoolExecutor},
074     *      the poolSize will be determined by {@link ThreadPoolExecutor#getMaximumPoolSize()}.
075     *      The default threadPool uses an unbounded queue to accept an unlimited
076     *      number of requests.</dd>
077     *  <dt>poolSize</dt><dd>Max number of concurrent requests</dd>
078     *  <dt>uri</dt><dd>Default request URI</dd>
079     *  <dt>contentType</dt><dd>Default content type for requests and responses</dd>
080     *  <dt>timeout</dt><dd>Timeout in milliseconds to wait for a connection to
081     *      be established and request to complete.</dd>
082     * </dl>
083     */
084    public AsyncHTTPBuilder( Map<String, ?> args ) throws URISyntaxException {
085        int poolSize = DEFAULT_POOL_SIZE;
086        ExecutorService threadPool = null;
087        if ( args != null ) {
088            threadPool = (ExecutorService)args.remove( "threadPool" );
089
090            if ( threadPool instanceof ThreadPoolExecutor )
091                poolSize = ((ThreadPoolExecutor)threadPool).getMaximumPoolSize();
092
093            Object poolSzArg = args.remove("poolSize");
094            if ( poolSzArg != null ) poolSize = Integer.parseInt( poolSzArg.toString() );
095
096            if ( args.containsKey( "url" ) ) throw new IllegalArgumentException(
097                "The 'url' parameter is deprecated; use 'uri' instead" );
098            Object defaultURI = args.remove("uri");
099            if ( defaultURI != null ) super.setUri(defaultURI);
100
101            Object defaultContentType = args.remove("contentType");
102            if ( defaultContentType != null )
103                super.setContentType(defaultContentType);
104
105            Object timeout = args.remove( "timeout" );
106            if ( timeout != null ) setTimeout( (Integer) timeout );
107
108            if ( args.size() > 0 ) {
109                String invalidArgs = "";
110                for ( String k : args.keySet() ) invalidArgs += k + ",";
111                throw new IllegalArgumentException("Unexpected keyword args: " + invalidArgs);
112            }
113        }
114        this.initThreadPools( poolSize, threadPool );
115    }
116
117    /**
118     * Submits a {@link Callable} instance to the job pool, which in turn will
119     * call {@link HTTPBuilder#doRequest(RequestConfigDelegate)} in an asynchronous
120     * thread.  The {@link Future} instance returned by this value (which in
121     * turn should be returned by any of the public <code>request</code> methods
122     * (including <code>get</code> and <code>post</code>) may be used to
123     * retrieve whatever value may be returned from the executed response
124     * handler closure.
125     */
126    @Override
127    protected Future<?> doRequest( final RequestConfigDelegate delegate ) {
128        return threadPool.submit( new Callable<Object>() {
129            /*@Override*/ public Object call() throws Exception {
130                try {
131                    return doRequestSuper(delegate);
132                }
133                catch( Exception ex ) {
134                    log.info( "Exception thrown from response delegate: " + delegate, ex );
135                    throw ex;
136                }
137            }
138        });
139    }
140
141    /*
142     * Because we can't call "super.doRequest" from within the anonymous
143     * Callable subclass.
144     */
145    private Object doRequestSuper( RequestConfigDelegate delegate ) throws IOException {
146        return super.doRequest(delegate);
147    }
148
149    /**
150     * Initializes threading parameters for the HTTPClient's
151     * {@link ThreadSafeClientConnManager}, and this class' ThreadPoolExecutor.
152     */
153    protected void initThreadPools( final int poolSize, final ExecutorService threadPool ) {
154        if (poolSize < 1) throw new IllegalArgumentException("poolSize may not be < 1");
155        // Create and initialize HTTP parameters
156        HttpParams params = super.getClient().getParams();
157        ConnManagerParams.setMaxTotalConnections(params, poolSize);
158        ConnManagerParams.setMaxConnectionsPerRoute(params,
159                new ConnPerRouteBean(poolSize));
160
161        HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
162
163        // Create and initialize scheme registry
164        SchemeRegistry schemeRegistry = new SchemeRegistry();
165        schemeRegistry.register( new Scheme( "http",
166                PlainSocketFactory.getSocketFactory(), 80 ) );
167        schemeRegistry.register( new Scheme( "https",
168                SSLSocketFactory.getSocketFactory(), 443));
169
170        ClientConnectionManager cm = new ThreadSafeClientConnManager(
171                params, schemeRegistry );
172        setClient(new DefaultHttpClient( cm, params ));
173
174        this.threadPool = threadPool != null ? threadPool :
175            new ThreadPoolExecutor( poolSize, poolSize, 120, TimeUnit.SECONDS,
176                    new LinkedBlockingQueue<Runnable>() );
177    }
178
179    /**
180     * {@inheritDoc}
181     */
182    @Override
183    protected Object defaultSuccessHandler( HttpResponseDecorator resp, Object parsedData )
184            throws ResponseParseException {
185        return super.defaultSuccessHandler( resp, parsedData );
186    }
187
188    /**
189     * For 'failure' responses (e.g. a 404), the exception will be wrapped in
190     * a {@link ExecutionException} and held by the {@link Future} instance.
191     * The exception is then re-thrown when calling {@link Future#get()
192     * future.get()}.  You can access the original exception (e.g. an
193     * {@link HttpResponseException}) by calling <code>ex.getCause()</code>.
194     *
195     */
196    @Override
197    protected void defaultFailureHandler( HttpResponseDecorator resp )
198            throws HttpResponseException {
199        super.defaultFailureHandler( resp );
200    }
201
202    /**
203     * This timeout is used for both the time to wait for an established
204     * connection, and the time to wait for data.
205     * @see HttpConnectionParams#setSoTimeout(HttpParams, int)
206     * @see HttpConnectionParams#setConnectionTimeout(HttpParams, int)
207     * @param timeout time to wait in milliseconds.
208     */
209    public void setTimeout( int timeout ) {
210        HttpConnectionParams.setConnectionTimeout( super.getClient().getParams(), timeout );
211        HttpConnectionParams.setSoTimeout( super.getClient().getParams(), timeout );
212        /* this will cause a thread waiting for an available connection instance
213         * to time-out   */
214//      ConnManagerParams.setTimeout( super.getClient().getParams(), timeout );
215    }
216
217    /**
218     * Get the timeout in for establishing an HTTP connection.
219     * @return timeout in milliseconds.
220     */
221    public int getTimeout() {
222        return HttpConnectionParams.getConnectionTimeout( super.getClient().getParams() );
223    }
224
225    /**
226     * <p>Access the underlying threadpool to adjust things like job timeouts.</p>
227     *
228     * <p>Note that this is not the same pool used by the HttpClient's
229     * {@link ThreadSafeClientConnManager}.  Therefore, increasing the
230     * {@link ThreadPoolExecutor#setMaximumPoolSize(int) maximum pool size} will
231     * not in turn increase the number of possible concurrent requests.  It will
232     * simply cause more requests to be <i>attempted</i> which will then simply
233     * block while waiting for a free connection.</p>
234     *
235     * @return the service used to execute requests.  By default this is a
236     * {@link ThreadPoolExecutor}.
237     */
238    public ExecutorService getThreadExecutor() {
239        return this.threadPool;
240    }
241
242    /**
243     * {@inheritDoc}
244     */
245    @Override public void shutdown() {
246        super.shutdown();
247        this.threadPool.shutdown();
248    }
249
250    /**
251     * {@inheritDoc}
252     * @see #shutdown()
253     */
254    @Override protected void finalize() throws Throwable {
255        this.shutdown();
256        super.finalize();
257    }
258}