/*
 * @(#)RetryModule.java					0.3-3 06/05/2001
 *
 *  This file is part of the HTTPClient package
 *  Copyright (C) 1996-2001 Ronald Tschalär
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free
 *  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *  MA 02111-1307, USA
 *
 *  For questions, suggestions, bug-reports, enhancement-requests etc.
 *  I may be contacted at:
 *
 *  ronald@innovation.ch
 *
 *  The HTTPClient's home page is located at:
 *
 *  http://www.innovation.ch/java/HTTPClient/ 
 *
 */

package HTTPClient;

import java.io.IOException;


/**
 * This module handles request retries when a connection closes prematurely.
 * It is triggered by the RetryException thrown by the StreamDemultiplexor.
 *
 * <P>This module is somewhat unique in that it doesn't strictly limit itself
 * to the HTTPClientModule interface and its return values. That is, it
 * sends request directly using the HTTPConnection.sendRequest() method. This
 * is necessary because this module will not only resend its request but it
 * also resend all other requests in the chain. Also, it rethrows the
 * RetryException in Phase1 to restart the processing of the modules.
 *
 * @version	0.3-3  06/05/2001
 * @author	Ronald Tschalär
 * @since	V0.3
 */
class RetryModule implements HTTPClientModule, GlobalConstants
{
    // Constructors

    /**
     */
    RetryModule()
    {
    }


    // Methods

    /**
     * Invoked by the HTTPClient.
     */
    public int requestHandler(Request req, Response[] resp)
    {
	return REQ_CONTINUE;
    }


    /**
     * Invoked by the HTTPClient.
     */
    public void responsePhase1Handler(Response resp, RoRequest roreq)
	    throws IOException, ModuleException
    {
	try
	{
	    resp.getStatusCode();
	}
	catch (RetryException re)
	{
	    Log.write(Log.MODS, "RtryM: Caught RetryException");

	    boolean got_lock = false;

	    try
	    {
	    synchronized (re.first)
	    {
		got_lock = true;

		// initialize idempotent sequence checking
		IdempotentSequence seq = new IdempotentSequence();
		for (RetryException e=re.first; e!=null; e=e.next)
		    seq.add(e.request);

		for (RetryException e=re.first; e!=null; e=e.next)
		{
		    Log.write(Log.MODS, "RtryM: handling exception ", e);

		    Request req = e.request;
		    HTTPConnection con = req.getConnection();

		    /* Don't retry if either the sequence is not idempotent
		     * (Sec 8.1.4 and 9.1.2), or we've already retried enough
		     * times, or the headers have been read and parsed
		     * already
		     */
		    if (!seq.isIdempotent(req)  ||
			(con.ServProtVersKnown  &&
			 con.ServerProtocolVersion >= HTTP_1_1  &&
			 req.num_retries > 0)  ||
			((!con.ServProtVersKnown  ||
			  con.ServerProtocolVersion <= HTTP_1_0)  &&
			 req.num_retries > 4)  ||
			e.response.got_headers)
		    {
			e.first = null;
			continue;
		    }


		    /**
		     * if an output stream was used (i.e. we don't have the
		     * data to resend) then delegate the responsibility for
		     * resending to the application.
		     */
		    if (req.getStream() != null)
		    {
			if (HTTPConnection.deferStreamed)
			{
			    req.getStream().reset();
			    e.response.setRetryRequest(true);
			}
			e.first = null;
			continue;
		    }


		    /* If we have an entity then setup either the entity-delay
		     * or the Expect header
		     */
		    if (req.getData() != null  &&  e.conn_reset)
		    {
			if (con.ServProtVersKnown  &&
			    con.ServerProtocolVersion >= HTTP_1_1)
			    addToken(req, "Expect", "100-continue");
			else
			    req.delay_entity = 5000L << req.num_retries;
		    }

		    /* If the next request in line has an entity and we're
		     * talking to an HTTP/1.0 server then close the socket
		     * after this request. This is so that the available()
		     * call (to watch for an error response from the server)
		     * will work correctly.
		     */
		    if (e.next != null  &&  e.next.request.getData() != null  &&
			(!con.ServProtVersKnown  ||
			 con.ServerProtocolVersion < HTTP_1_1)  &&
			 e.conn_reset)
		    {
			addToken(req, "Connection", "close");
		    }


		    /* If this an HTTP/1.1 server then don't pipeline retries.
		     * The problem is that if the server for some reason
		     * decides not to use persistent connections and it does
		     * not do a correct shutdown of the connection, then the
		     * response will be ReSeT. If we did pipeline then we
		     * would keep falling into this trap indefinitely.
		     *
		     * Note that for HTTP/1.0 servers, if they don't support
		     * keep-alives then the normal code will already handle
		     * this accordingly and won't pipe over the same
		     * connection.
		     */
		    if (con.ServProtVersKnown  &&
			con.ServerProtocolVersion >= HTTP_1_1  &&
			e.conn_reset)
		    {
			req.dont_pipeline = true;
		    }
		    // The above is too risky - for moment let's be safe
		    // and never pipeline retried request at all.
		    req.dont_pipeline = true;


		    // now resend the request

		    Log.write(Log.MODS, "RtryM: Retrying request '" +
					req.getMethod() + " " +
					req.getRequestURI() + "'");

		    if (e.conn_reset)
			req.num_retries++;
		    e.response.http_resp.set(req,
				    con.sendRequest(req, e.response.timeout));
		    e.exception = null;
		    e.first = null;
		}
	    }
	    }
	    catch (NullPointerException npe)
		{ if (got_lock)  throw npe; }
	    catch (ParseException pe)
		{ throw new IOException(pe.getMessage()); }

	    if (re.exception != null)  throw re.exception;

	    re.restart = true;
	    throw re;
	}
    }


    /**
     * Invoked by the HTTPClient.
     */
    public int responsePhase2Handler(Response resp, Request req)
    {
	// reset any stuff we might have set previously
	req.delay_entity  = 0;
	req.dont_pipeline = false;
	req.num_retries   = 0;

	return RSP_CONTINUE;
    }


    /**
     * Invoked by the HTTPClient.
     */
    public void responsePhase3Handler(Response resp, RoRequest req)
    {
    }


    /**
     * Invoked by the HTTPClient.
     */
    public void trailerHandler(Response resp, RoRequest req)
    {
    }


    /**
     * Add a token to the given header. If the header does not exist then
     * create it with the given token.
     *
     * @param req the request who's headers are to be modified
     * @param hdr the name of the header to add the token to (or to create)
     * @param tok the token to add
     * @exception ParseException if parsing the header fails
     */
    private void addToken(Request req, String hdr, String tok)
	    throws ParseException
    {
	int idx;
	NVPair[] hdrs = req.getHeaders();
	for (idx=0; idx<hdrs.length; idx++)
	{
	    if (hdrs[idx].getName().equalsIgnoreCase(hdr))
		break;
	}

	if (idx == hdrs.length)		// no such header, so add one
	{
	    hdrs = Util.resizeArray(hdrs, idx+1);
	    hdrs[idx] = new NVPair(hdr, tok);
	    req.setHeaders(hdrs);
	}
	else					// header exists, so add token
	{
	    if (!Util.hasToken(hdrs[idx].getValue(), tok))
		hdrs[idx] = new NVPair(hdr, hdrs[idx].getValue() + ", " + tok);
	}
    }
}
