001    /*
002     * Copyright 1999-2002,2004 The Apache Software Foundation.
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    
017    package org.apache.commons.latka.http;
018    // java imports
019    import java.net.URL;
020    import java.io.ByteArrayInputStream;
021    import java.io.InputStream;
022    import java.io.IOException;
023    import java.util.List;
024    import java.util.LinkedList;
025    // latka imports
026    import org.apache.commons.httpclient.HostConfiguration;
027    import org.apache.commons.httpclient.UsernamePasswordCredentials;
028    import org.apache.commons.httpclient.HttpClient;
029    import org.apache.commons.httpclient.HttpException;
030    import org.apache.commons.httpclient.HttpMethod;
031    import org.apache.commons.httpclient.HttpState;
032    import org.apache.commons.httpclient.HttpMethodBase;
033    import org.apache.commons.httpclient.methods.PostMethod;
034    // log4j imports
035    import org.apache.log4j.Category;
036    
037    /**
038     * An implementation of a Latka Request interface based on the Apache Commons
039     * HttpClient package.
040     *
041     * @todo pass proxy host and port to httpclient
042     * @author <a href="mailto:dsale@us.britannica.com">Doug Sale</a>
043     * @author <a href="mailto:mdelagra@us.britannica.com">Morgan Delagrange</a>
044     * @author dIon Gillard
045     * @version $Id: RequestImpl.java 561366 2007-07-31 15:58:29Z rahul $
046     * @see Request
047     */
048    public class RequestImpl implements Request {
049        
050        /** Standard HTTP Port */
051        public static final int HTTP_PORT = 80;
052        
053        /** Standard HTTPS Port */
054        public static final int HTTPS_PORT = 443;
055        /** host the request is being made on */
056        protected String  _host   = null;
057        /** port the request is being made on */
058        protected int     _port   = -1;
059        /** http method being used to make the request */
060        protected int     _method = -1;
061        /** http session the request is part of */
062        protected SessionImpl _session    = null;
063        /** credentials for the request */
064        protected Credentials _credentials = null;
065        /** http method implementation */
066        protected HttpMethod  _httpMethod = null;
067        /** URL being requested */
068        protected URL         _targetURL  = null;
069        /** query string portion of the URL */
070        protected String      _query      = null;
071        /** time taken for the request in milliseconds */
072        protected long        _requestTiming = -1;
073        /** name given to the request */
074        protected String      _label      = null;
075        /** headers sent with this request */
076        protected RequestHeaders _requestHeaders = new RequestHeadersImpl();
077        /** parameters sent with this request */
078        protected Parameters _parameters = new ParametersImpl();
079        /** manually constructed post body */
080        protected String _requestBody = null;
081        /** whether or not redirect responses should be followed as part of the
082            request processing*/
083        protected boolean _followRedirects = true;
084        /** object used to perform the http requests */
085        protected HttpClient _httpClient = new HttpClient();
086        /** proxy used to perform the requests */
087        private Proxy _proxy = null;
088        /** log4j category used when logging messages */
089        protected static final Category _log = 
090            Category.getInstance(RequestImpl.class);
091    
092        /**
093         * HTTP Version 1.0
094         */
095        private static final String HTTP_10 = "1.0";
096        /**
097         * HTTP Version 1.1
098         */    
099        private static final String HTTP_11 = "1.1";
100    
101        protected String _httpVersion = HTTP_11;
102    
103        protected List _visitedURLs = new LinkedList();
104    
105        /**
106         * Create a request for the specified URL, process using the supplied method
107         * and use the supplied state and session for persistent data
108         * @param url the URL to send the request to
109         * @param httpMethod the http method to use in sending the request, as 
110         *        defined in {@link Request}
111         * @param state shared information across requests
112         * @param session state shared across servers and requests
113         */
114        protected RequestImpl(URL url, int httpMethod, HttpState state,
115            SessionImpl session) {
116            this(null, url, httpMethod, state, session, true);
117        }
118        
119        /**
120         * Create a RequestImpl
121         *
122         * @param label       a name for the request
123         * @param url         the url that this request embodies
124         * @param httpMethod  the method by which this request should be executed
125         * @param state       shared information across requests
126         * @param session     the session that the request should be executed in
127         * @param followRedirects whether http redirect responses should be honoured
128         */
129        protected RequestImpl(String label, URL url, int httpMethod, 
130            HttpState state, SessionImpl session, boolean followRedirects) {
131                
132            _followRedirects = followRedirects;
133            _method = httpMethod;
134            _httpClient.setState(state);
135            
136            _label = label;
137            _query = url.getQuery();
138            
139            _session = session;
140            _targetURL = url;
141            _httpMethod = MethodFactory.getInstance(httpMethod, url);
142            if (_query != null) {
143                _httpMethod.setQueryString(_query);
144            }
145            _httpMethod.setFollowRedirects(followRedirects);
146        }
147        
148        /**
149         * Returns the object implementing the HttpMethod
150         * (get, post, etc.) for this request.
151         *
152         * @return the underlying HttpMethod object representing the request/
153         *      response pair.
154         */
155        protected HttpMethod getHttpMethod() {
156            return _httpMethod;
157        }
158        
159        /**
160         * Defined in the implemented interface
161         * @return the headers used for this request
162         * @see Request#getHeaders()
163         */
164        public RequestHeaders getHeaders() {
165            return _requestHeaders;
166        }
167        
168        /**
169         * Defined in the implemented interface
170         * @param requestHeaders new value for the headers property
171         * @see Request#setHeaders(RequestHeaders)
172         */
173        public void setHeaders(RequestHeaders requestHeaders) {
174            _requestHeaders = requestHeaders;
175        }
176        
177        /**
178         * Defined in the implemented interface
179         * @return the parameters property
180         * @see Request#getParameters()
181         */
182        public Parameters getParameters() {
183            return _parameters;
184        }
185    
186        /**
187         * Defined in the implemented interface
188         * @see Request#setRequestBody(String)
189         */
190        public void setRequestBody(String body) {
191            _requestBody = body;
192        }
193        
194        /**
195         * Defined in the implemented interface
196         * @param parameters new value for parameters property
197         * @see Request#setParameters(Parameters)
198         */
199        public void setParameters(Parameters parameters) {
200            _parameters = parameters;
201        }
202        
203        /**
204         * Defined in the implemented interface
205         * @param credentials username and password to use
206         * @see Request#setCredentials(Credentials)
207         */
208        public void setCredentials(Credentials credentials) {
209            // null implies that this credential is the default (vs. specifying a 
210            // realm)
211            UsernamePasswordCredentials creds = new UsernamePasswordCredentials(
212                credentials.getUserName(), credentials.getPassword());
213            _session._state.setCredentials(null, creds);
214            _credentials = credentials;
215        }
216    
217        public Credentials getCredentials() {
218            return _credentials;
219        }
220        
221        /**
222         * Execute the request - perform the http interaction.
223         * Since HttpClient doesn't follow off-server
224         * redirects, execute() may have to construct further 
225         * requests that call this method.
226         * 
227         * @return a {@link Response} detailing the html etc
228         * @exception IOException
229         *                   when there are problems reading and writing
230         * @see Request#execute()
231         */
232        protected Response executeRequestPerHost() throws IOException {
233            
234            // set the request headers in HTTPClient
235            List headers = _requestHeaders.getHeaders();
236            for (int i = 0; i < headers.size(); ++i) {
237                String[] header = (String[]) headers.get(i);
238                _httpMethod.addRequestHeader(header[0], header[1]);
239            }
240            
241            if (_requestBody != null) {
242                InputStream  is = new ByteArrayInputStream(
243                    _requestBody.getBytes("ISO-8859-1"));
244                ((PostMethod) _httpMethod).setRequestBody(is);
245            } else {
246                List parameters = _parameters.getParameters();
247                for (int i = 0; i < parameters.size(); ++i) {
248                    String[] parameter = (String[]) parameters.get(i);
249                    addHttpClientParameter(parameter[0], parameter[1]);
250                }
251            }
252            
253            // for timing
254            long startDate = System.currentTimeMillis();
255            
256            Response response = null;
257            try {
258                // open the connection
259                openConnection();
260                
261                _log.debug("executing request");
262                
263                _httpClient.executeMethod(_httpMethod);
264                
265                _log.debug("request executed");
266                
267                response = new ResponseImpl(this);
268    
269                // Cache the response body to allow it to be accessed
270                // after the http connection is closed.
271                String sideEffectOnly = response.getResource();
272                
273                // set the referer
274                // note: If followRedirects
275                // is enabled, HTTPClient may return a path
276                // that is different from the initial request.
277                // HTTPClient will not follow redirects to another
278                // host, port, or protocol; in that event, it will always
279                // return a 301 or 302.
280                _session.setReferer(new URL(_targetURL.getProtocol(), _host, _port,
281                    _httpMethod.getPath()));
282                
283            } catch (HttpException e) {
284                throw new IOException(e.toString());
285            } catch (IOException e) {
286                // rethrow it after closing the connection
287                throw e;
288            } finally {
289                try {
290                    closeConnection(); 
291                    // FIXME: Shouldn't use Exception here.
292                } catch (Exception e) {
293                    e.printStackTrace();
294                }
295            }
296            
297            
298            _requestTiming = System.currentTimeMillis() - startDate;
299            
300            if (_log.isInfoEnabled()) {
301                _log.info("response obtained (response logging disabled because "
302                    + "some responses are binary)");
303            }
304            
305            return response;
306        }
307        
308        /**
309         * Executes the request.  In the event of a 301 or 302,
310         * this method may need to create a new Request, 
311         * due to an idiosyncracy of
312         * HttpClient.  In that case, the Response.getRequest()
313         * method will return the actual Request that was executed.
314         */
315        public Response execute() throws IOException {
316            Response response = executeRequestPerHost();
317    
318            if (followRedirects() == false) {
319                return response;
320            }
321    
322            Request lastRequest = this;
323            // execute the request until either we get a non-redirect response, or
324            // we visit a URL we have already visited
325            while (response.getStatusCode() == 301 || response.getStatusCode() == 302) {
326                // follow the redirect
327                URL url = new URL(response.getHeader("location"));
328    
329                if (_visitedURLs.contains(url.toString())) {
330                    return response;
331                }
332    
333                Request request = _session.createRequest(lastRequest.getLabel(), url,
334                    lastRequest.getMethod(), lastRequest.getVersion(), true, getProxy());
335                request.setParameters(lastRequest.getParameters());
336                request.setHeaders(lastRequest.getHeaders());
337                Credentials credentials = lastRequest.getCredentials();
338                if (credentials != null) {
339                    request.setCredentials(credentials);
340                }
341                response = request.execute();
342                _visitedURLs.add(url.toString());
343                lastRequest = request;
344            }
345    
346            return response;
347        }
348    
349        /**
350         * Get the URL used for the request
351         * @return the {@link URL} of the request
352         * @see Request#getURL
353         */
354        public URL getURL() {
355            return _targetURL;
356        }
357        
358        /**
359         * Get the label used for the request
360         * @return the label used to create the request
361         * @see Request#getLabel
362         */
363        public String getLabel() {
364            return _label;
365        }
366        
367        /**
368         * Add a parameter (name and value) to the request
369         * @param name the name of the parameter to be added
370         * @param value the value of the parameter to be added
371         * @see Request#addParameter(String,String)
372         */
373        public void addParameter(String name, String value) {
374            _parameters.addParameter(name, value);
375        }
376        
377        /**
378         * Associate a parameter with this request.
379         *
380         * @param name  the lvalue of the parameter - must not be null
381         * @param value  the rvalue of the parameter - must not be null
382         */
383        protected void addHttpClientParameter(String name, String value) {
384            if (name == null) {
385                throw new NullPointerException("name parameter is null");
386            }
387            
388            if (value == null) {
389                throw new NullPointerException("value parameter is null");
390            }
391            _log.info("adding parameter, name: " + name + ", value: " + value);
392            
393            if (_httpMethod instanceof PostMethod) {
394                // addParameter adds to POST Entity, not URL
395                ((PostMethod) _httpMethod).addParameter(name, value);
396            } else {
397                StringBuffer query = new StringBuffer();
398    
399                // setParameter adds to URL as query string
400                if (_query == null || _query.equals("")) {
401                    query.append(name);
402                    query.append("=");
403                    query.append(value);
404                } else {
405                    query.append(_query);
406                    query.append("&");
407                    query.append(name);
408                    query.append("=");
409                    query.append(value);
410                }
411                _query = query.toString();
412                ((HttpMethod) _httpMethod).setQueryString(_query);
413            }
414        }
415        
416        /**
417         * Set a header in the request
418         *
419         * @param headerName name of any HTTP request header
420         * @param headerValue value of that header
421         */
422        public void addHeader(String headerName, String headerValue) {
423            _requestHeaders.addHeader(headerName, headerValue);
424        }
425        
426        /**
427         * Retrieve the session associated with this request.
428         *
429         * @return a <code>Session</code> object
430         */
431        public Session getSession() {
432            return _session;
433        }
434        
435        /**
436         * @return the time it took to execute this request in milliseconds
437         * or -1 if the request has not yet been executed
438         */
439        public int getRequestTiming() {
440            return (int) _requestTiming;
441        }
442        
443        /**
444         * opens an HTTP connection.  This method is called
445         * before executing the method.
446         *
447         * @exception IOException
448         *                   if the server could not be contacted
449         */
450        protected void openConnection() throws IOException {
451            _log.debug("Opening connection");
452            
453            URL url = getURL();
454            String protocol = url.getProtocol();
455            String host = url.getHost();
456            int port = url.getPort();
457            
458            // explicitly set port if not in url,
459            // just for storing away and comparison
460            if (port == -1) {
461                if (protocol.equals("http")) {
462                    port = HTTP_PORT;
463                } else if (protocol.equals("https")) {
464                    port = HTTPS_PORT;
465                } else {
466                    throw new IllegalArgumentException("Unsupported Protocol");
467                }
468            }
469            
470            // save session values
471            _host = host;
472            _port = port;
473            
474            HostConfiguration hostConfiguration = _httpClient.getHostConfiguration();
475            hostConfiguration.setHost(host, port, protocol);
476            if (getProxy() != null) {
477                hostConfiguration.setProxy(getProxy().getHost(), getProxy().getPort());
478            }
479            
480            _log.debug("connection open");
481        }
482        
483        /**
484         * Closes the http connection associated with the request
485         * @throws IOException if there are problems closing the connection
486         */
487        protected void closeConnection() throws IOException {
488            _log.debug("closing connection");
489            _httpMethod.releaseConnection();
490            _log.debug("connection closed");
491        }
492        
493        /**
494         * Defined in the interface
495         * @return whether the request will honour http redirect status codes
496         * @see Request#followRedirects()
497         */
498        public boolean followRedirects() {
499            return _followRedirects;
500        }
501        
502        /**
503         * Defined in the interface
504         * @return the http method being used for the request, as defined in the
505         *      interface
506         * @see Request#getMethod
507         */
508        public int getMethod() {
509            return _method;
510        }
511        
512        /** Getter for property proxy.
513         * @return Value of property proxy.
514         */
515        public Proxy getProxy() {
516            return _proxy;
517        }
518        
519        /** Setter for property proxy.
520         * @param proxy New value of property proxy.
521         */
522        public void setProxy(Proxy proxy) {
523            _proxy = proxy;
524        }
525        
526        /**
527         * Sets the HTTP version for this request.  If
528         * the version provided is not 1.0 or 1.1, then
529         * 1.1 will be used.
530         *
531         * @param version  HTTP version
532         */
533        public void setVersion(String version) {
534            ((HttpMethodBase) _httpMethod).setHttp11(!HTTP_10.equals(version));
535            _httpVersion = version;
536        }
537    
538        /**
539         * Get the HTTP version to use in this request.
540         * 
541         * @return HTTP version for the request
542         */
543        public String getVersion() {
544            return _httpVersion;
545        }
546    }