/*
 * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/EntityEnclosingMethod.java,v 1.39 2004/07/03 14:27:03 olegk Exp $
 * $Revision: 480424 $
 * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $
 *
 * ====================================================================
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.commons.httpclient.methods;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;

import org.apache.commons.httpclient.ChunkedOutputStream;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpConnection;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.HttpVersion;
import org.apache.commons.httpclient.ProtocolException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This abstract class serves as a foundation for all HTTP methods 
 * that can enclose an entity within requests 
 *
 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
 * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
 *
 * @since 2.0beta1
 * @version $Revision: 480424 $
 */
public abstract class EntityEnclosingMethod extends ExpectContinueMethod {

    // ----------------------------------------- Static variables/initializers

    /**
     * The content length will be calculated automatically. This implies
     * buffering of the content.
     * @deprecated Use {@link InputStreamRequestEntity#CONTENT_LENGTH_AUTO}.
     */
    public static final long CONTENT_LENGTH_AUTO = InputStreamRequestEntity.CONTENT_LENGTH_AUTO;

    /**
     * The request will use chunked transfer encoding. Content length is not
     * calculated and the content is not buffered.<br>
     * @deprecated Use {@link #setContentChunked(boolean)}.
     */
    public static final long CONTENT_LENGTH_CHUNKED = -1;

    /** LOG object for this class. */
    private static final Log LOG = LogFactory.getLog(EntityEnclosingMethod.class);

    /** The unbuffered request body, if any. */
    private InputStream requestStream = null;

    /** The request body as string, if any. */
    private String requestString = null;

    private RequestEntity requestEntity;
    
    /** Counts how often the request was sent to the server. */
    private int repeatCount = 0;

    /** The content length of the <code>requestBodyStream</code> or one of
     *  <code>CONTENT_LENGTH_AUTO</code> and <code>CONTENT_LENGTH_CHUNKED</code>.
     * 
     * @deprecated
     */
    private long requestContentLength = InputStreamRequestEntity.CONTENT_LENGTH_AUTO;
    
    private boolean chunked = false;

    // ----------------------------------------------------------- Constructors

    /**
     * No-arg constructor.
     *
     * @since 2.0
     */
    public EntityEnclosingMethod() {
        super();
        setFollowRedirects(false);
    }

    /**
     * Constructor specifying a URI.
     *
     * @param uri either an absolute or relative URI
     *
     * @since 2.0
     */
    public EntityEnclosingMethod(String uri) {
        super(uri);
        setFollowRedirects(false);
    }

    /**
     * Returns <tt>true</tt> if there is a request body to be sent.
     * 
     * <P>This method must be overridden by sub-classes that implement
     * alternative request content input methods
     * </p>
     * 
     * @return boolean
     * 
     * @since 2.0beta1
     */
    protected boolean hasRequestContent() {
        LOG.trace("enter EntityEnclosingMethod.hasRequestContent()");
        return (this.requestEntity != null) 
            || (this.requestStream != null) 
            || (this.requestString != null);
    }

    /**
     * Clears the request body.
     * 
     * <p>This method must be overridden by sub-classes that implement
     * alternative request content input methods.</p>
     * 
     * @since 2.0beta1
     */
    protected void clearRequestBody() {
        LOG.trace("enter EntityEnclosingMethod.clearRequestBody()");
        this.requestStream = null;
        this.requestString = null;
        this.requestEntity = null;
    }

    /**
     * Generates the request body.   
     * 
     * <p>This method must be overridden by sub-classes that implement
     * alternative request content input methods.</p>
     * 
     * @return request body as an array of bytes. If the request content 
     *          has not been set, returns <tt>null</tt>.
     * 
     * @since 2.0beta1
     */
    protected byte[] generateRequestBody() {
        LOG.trace("enter EntityEnclosingMethod.renerateRequestBody()");
        return null;
    }

    protected RequestEntity generateRequestEntity() {
        
        byte[] requestBody = generateRequestBody();
        if (requestBody != null) {
            // use the request body, if it exists.
            // this is just for backwards compatability
            this.requestEntity = new ByteArrayRequestEntity(requestBody);
        } else if (this.requestStream != null) {
            this.requestEntity = new InputStreamRequestEntity(
                requestStream, 
                requestContentLength);
            this.requestStream = null;
        } else if (this.requestString != null) {
            String charset = getRequestCharSet(); 
            try {
                this.requestEntity = new StringRequestEntity(
                        requestString, null, charset);
            } catch (UnsupportedEncodingException e) {
                if (LOG.isWarnEnabled()) {
                    LOG.warn(charset + " not supported");
                }
                try {
                    this.requestEntity = new StringRequestEntity(
                            requestString, null, null);
                } catch (UnsupportedEncodingException ignore) {
                }
            }
        }

        return this.requestEntity;
    }
    
    /**
     * Entity enclosing requests cannot be redirected without user intervention
     * according to RFC 2616.
     *
     * @return <code>false</code>.
     *
     * @since 2.0
     */
    public boolean getFollowRedirects() {
        return false;
    }


    /**
     * Entity enclosing requests cannot be redirected without user intervention 
     * according to RFC 2616.
     *
     * @param followRedirects must always be <code>false</code>
     */
    public void setFollowRedirects(boolean followRedirects) {
        if (followRedirects == true) {
            throw new IllegalArgumentException("Entity enclosing requests cannot be redirected without user intervention");
        }
        super.setFollowRedirects(false);
    }

    /**
     * Sets length information about the request body.
     *
     * <p>
     * Note: If you specify a content length the request is unbuffered. This
     * prevents redirection and automatic retry if a request fails the first
     * time. This means that the HttpClient can not perform authorization
     * automatically but will throw an Exception. You will have to set the
     * necessary 'Authorization' or 'Proxy-Authorization' headers manually.
     * </p>
     *
     * @param length size in bytes or any of CONTENT_LENGTH_AUTO,
     *        CONTENT_LENGTH_CHUNKED. If number of bytes or CONTENT_LENGTH_CHUNKED
     *        is specified the content will not be buffered internally and the
     *        Content-Length header of the request will be used. In this case
     *        the user is responsible to supply the correct content length.
     *        If CONTENT_LENGTH_AUTO is specified the request will be buffered
     *        before it is sent over the network.
     * 
     * @deprecated Use {@link #setContentChunked(boolean)} or 
     * {@link #setRequestEntity(RequestEntity)}
     */
    public void setRequestContentLength(int length) {
        LOG.trace("enter EntityEnclosingMethod.setRequestContentLength(int)");
        this.requestContentLength = length;
    }

    /**
     * Returns the request's charset.  The charset is parsed from the request entity's 
     * content type, unless the content type header has been set manually. 
     * 
     * @see RequestEntity#getContentType()
     * 
     * @since 3.0
     */
    public String getRequestCharSet() {
        if (getRequestHeader("Content-Type") == null) {
            // check the content type from request entity
            // We can't call getRequestEntity() since it will probably call
            // this method.
            if (this.requestEntity != null) {
                return getContentCharSet(
                    new Header("Content-Type", requestEntity.getContentType()));
            } else {
                return super.getRequestCharSet();
            }
        } else {
            return super.getRequestCharSet();
        }
    }

    /**
     * Sets length information about the request body.
     *
     * <p>
     * Note: If you specify a content length the request is unbuffered. This
     * prevents redirection and automatic retry if a request fails the first
     * time. This means that the HttpClient can not perform authorization
     * automatically but will throw an Exception. You will have to set the
     * necessary 'Authorization' or 'Proxy-Authorization' headers manually.
     * </p>
     *
     * @param length size in bytes or any of CONTENT_LENGTH_AUTO,
     *        CONTENT_LENGTH_CHUNKED. If number of bytes or CONTENT_LENGTH_CHUNKED
     *        is specified the content will not be buffered internally and the
     *        Content-Length header of the request will be used. In this case
     *        the user is responsible to supply the correct content length.
     *        If CONTENT_LENGTH_AUTO is specified the request will be buffered
     *        before it is sent over the network.
     * 
     * @deprecated Use {@link #setContentChunked(boolean)} or 
     * {@link #setRequestEntity(RequestEntity)}
     */
    public void setRequestContentLength(long length) {
        LOG.trace("enter EntityEnclosingMethod.setRequestContentLength(int)");
        this.requestContentLength = length;
    }

    /**
     * Sets whether or not the content should be chunked.
     * 
     * @param chunked <code>true</code> if the content should be chunked
     * 
     * @since 3.0
     */
    public void setContentChunked(boolean chunked) {
        this.chunked = chunked;
    }
    
    /**
     * Returns the length of the request body.
     *
     * @return number of bytes in the request body
     */
    protected long getRequestContentLength() {
        LOG.trace("enter EntityEnclosingMethod.getRequestContentLength()");

        if (!hasRequestContent()) {
            return 0;
        }
        if (this.chunked) {
            return -1;
        }
        if (this.requestEntity == null) {
            this.requestEntity = generateRequestEntity(); 
        }
        return (this.requestEntity == null) ? 0 : this.requestEntity.getContentLength();
    }

    /**
     * Populates the request headers map to with additional 
     * {@link org.apache.commons.httpclient.Header headers} to be submitted to 
     * the given {@link HttpConnection}.
     *
     * <p>
     * This implementation adds tt>Content-Length</tt> or <tt>Transfer-Encoding</tt>
     * headers.
     * </p>
     *
     * <p>
     * Subclasses may want to override this method to to add additional
     * headers, and may choose to invoke this implementation (via
     * <tt>super</tt>) to add the "standard" headers.
     * </p>
     *
     * @param state the {@link HttpState state} information associated with this method
     * @param conn the {@link HttpConnection connection} used to execute
     *        this HTTP method
     *
     * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
     *                     can be recovered from.
     * @throws HttpException  if a protocol exception occurs. Usually protocol exceptions 
     *                    cannot be recovered from.
     *
     * @see #writeRequestHeaders
     * 
     * @since 3.0
     */
    protected void addRequestHeaders(HttpState state, HttpConnection conn)
    throws IOException, HttpException {
        LOG.trace("enter EntityEnclosingMethod.addRequestHeaders(HttpState, "
            + "HttpConnection)");

        super.addRequestHeaders(state, conn);
        addContentLengthRequestHeader(state, conn);

        // only use the content type of the request entity if it has not already been
        // set manually
        if (getRequestHeader("Content-Type") == null) {
            RequestEntity requestEntity = getRequestEntity();
            if (requestEntity != null && requestEntity.getContentType() != null) {
                setRequestHeader("Content-Type", requestEntity.getContentType());
            }
        }
    }
    
    /**
     * Generates <tt>Content-Length</tt> or <tt>Transfer-Encoding: Chunked</tt>
     * request header, as long as no <tt>Content-Length</tt> request header
     * already exists.
     *
     * @param state current state of http requests
     * @param conn the connection to use for I/O
     *
     * @throws IOException when errors occur reading or writing to/from the
     *         connection
     * @throws HttpException when a recoverable error occurs
     */
    protected void addContentLengthRequestHeader(HttpState state,
                                                 HttpConnection conn)
    throws IOException, HttpException {
        LOG.trace("enter EntityEnclosingMethod.addContentLengthRequestHeader("
                  + "HttpState, HttpConnection)");

        if ((getRequestHeader("content-length") == null) 
            && (getRequestHeader("Transfer-Encoding") == null)) {
            long len = getRequestContentLength();
            if (len < 0) {
                if (getEffectiveVersion().greaterEquals(HttpVersion.HTTP_1_1)) {
                    addRequestHeader("Transfer-Encoding", "chunked");
                } else {
                    throw new ProtocolException(getEffectiveVersion() + 
                        " does not support chunk encoding");
                }
            } else {
                addRequestHeader("Content-Length", String.valueOf(len));
            }
        }
    }

    /**
     * Sets the request body to be the specified inputstream.
     *
     * @param body Request body content as {@link java.io.InputStream}
     * 
     * @deprecated use {@link #setRequestEntity(RequestEntity)}
     */
    public void setRequestBody(InputStream body) {
        LOG.trace("enter EntityEnclosingMethod.setRequestBody(InputStream)");
        clearRequestBody();
        this.requestStream = body;
    }

    /**
     * Sets the request body to be the specified string.
     * The string will be submitted, using the encoding
     * specified in the Content-Type request header.<br>
     * Example: <code>setRequestHeader("Content-type", "text/xml; charset=UTF-8");</code><br>
     * Would use the UTF-8 encoding.
     * If no charset is specified, the 
     * {@link org.apache.commons.httpclient.HttpConstants#DEFAULT_CONTENT_CHARSET default}
     * content encoding is used (ISO-8859-1).
     *
     * @param body Request body content as a string
     * 
     * @deprecated use {@link #setRequestEntity(RequestEntity)}
     */
    public void setRequestBody(String body) {
        LOG.trace("enter EntityEnclosingMethod.setRequestBody(String)");
        clearRequestBody();
        this.requestString = body;
    }

    /**
     * Writes the request body to the given {@link HttpConnection connection}.
     *
     * @param state the {@link HttpState state} information associated with this method
     * @param conn the {@link HttpConnection connection} used to execute
     *        this HTTP method
     *
     * @return <tt>true</tt>
     *
     * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
     *                     can be recovered from.
     * @throws HttpException  if a protocol exception occurs. Usually protocol exceptions 
     *                    cannot be recovered from.
     */
    protected boolean writeRequestBody(HttpState state, HttpConnection conn)
    throws IOException, HttpException {
        LOG.trace(
            "enter EntityEnclosingMethod.writeRequestBody(HttpState, HttpConnection)");
        
        if (!hasRequestContent()) {
            LOG.debug("Request body has not been specified");
            return true;
        }
        if (this.requestEntity == null) {
            this.requestEntity = generateRequestEntity(); 
        }
        if (requestEntity == null) {
            LOG.debug("Request body is empty");
            return true;
        }

        long contentLength = getRequestContentLength();

        if ((this.repeatCount > 0) && !requestEntity.isRepeatable()) {
            throw new ProtocolException(
                "Unbuffered entity enclosing request can not be repeated.");
        }

        this.repeatCount++;

        OutputStream outstream = conn.getRequestOutputStream();
        
        if (contentLength < 0) {
            outstream = new ChunkedOutputStream(outstream);
        }
        
        requestEntity.writeRequest(outstream);
        
        // This is hardly the most elegant solution to closing chunked stream
        if (outstream instanceof ChunkedOutputStream) {
            ((ChunkedOutputStream) outstream).finish();
        }
        
        outstream.flush();
        
        LOG.debug("Request body sent");
        return true;
    }

    /**
     * Recycles the HTTP method so that it can be used again.
     * Note that all of the instance variables will be reset
     * once this method has been called. This method will also
     * release the connection being used by this HTTP method.
     * 
     * @see #releaseConnection()
     * 
     * @deprecated no longer supported and will be removed in the future
     *             version of HttpClient
     */
    public void recycle() {
        LOG.trace("enter EntityEnclosingMethod.recycle()");
        clearRequestBody();
        this.requestContentLength = InputStreamRequestEntity.CONTENT_LENGTH_AUTO;
        this.repeatCount = 0;
        this.chunked = false;
        super.recycle();
    }

    /**
     * @return Returns the requestEntity.
     * 
     * @since 3.0
     */
    public RequestEntity getRequestEntity() {
        return generateRequestEntity();
    }

    /**
     * @param requestEntity The requestEntity to set.
     * 
     * @since 3.0
     */
    public void setRequestEntity(RequestEntity requestEntity) {
        clearRequestBody();
        this.requestEntity = requestEntity;
    }

}
