Package org.apache.excalibur.source.impl

Source Code of org.apache.excalibur.source.impl.HTTPClientSource$WrappedFileOutputStream

/*
* 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.
*/
package org.apache.excalibur.source.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;

import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.logger.Logger;
import org.apache.avalon.framework.parameters.ParameterException;
import org.apache.avalon.framework.parameters.Parameterizable;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.util.DateParseException;
import org.apache.commons.httpclient.util.DateUtil;
import org.apache.excalibur.source.ModifiableSource;
import org.apache.excalibur.source.Source;
import org.apache.excalibur.source.SourceException;
import org.apache.excalibur.source.SourceFactory;
import org.apache.excalibur.source.SourceNotFoundException;
import org.apache.excalibur.source.SourceParameters;
import org.apache.excalibur.source.SourceResolver;
import org.apache.excalibur.source.SourceUtil;
import org.apache.excalibur.source.SourceValidity;
import org.apache.excalibur.source.impl.validity.TimeStampValidity;

/**
* HTTP URL Source object, based on the Jakarta Commons
* <a href="http://jakarta.apache.org/commons/httpclient/">HttpClient</a>
* project.
*
* @author <a href="mailto:dev@avalon.apache.org">Avalon Development Team</a>
* @version CVS $Id: HTTPClientSource.java,v 1.4 2004/02/28 11:47:24 cziegeler Exp $
*/
public class HTTPClientSource extends AbstractLogEnabled
    implements ModifiableSource, Initializable, Parameterizable
{
    /**
     * Constant used for identifying POST requests.
     */
    public static final String POST           = "POST";

    /**
     * Constant used for identifying GET requests.
     */
    public static final String GET            = "GET";

    /**
     * Constant used for configuring the proxy hostname.
     */
    public static final String PROXY_HOST     = "proxy.host";

    /**
     * Constant used for configuring the proxy port number.
     */
    public static final String PROXY_PORT     = "proxy.port";

    /**
     * Constant used when obtaining the Content-Type from HTTP Headers
     */
    public static final String CONTENT_TYPE   = "Content-Type";

    /**
     * Constant used when obtaining the Content-Length from HTTP Headers
     */
    public static final String CONTENT_LENGTH = "Content-Length";

    /**
     * Constant used when obtaining the Last-Modified date from HTTP Headers
     */
    public static final String LAST_MODIFIED  = "Last-Modified";

    /**
     * The URI being accessed.
     */
    private final String m_uri;

    /**
     * Contextual parameters passed via the {@link SourceFactory}.
     */
    private final Map m_parameters;

    /**
     * Optional http state passed from SourceFactory
     */
    private final HttpState m_httpState;

    /**
     * The {@link HttpClient} object.
     */
    private HttpClient m_client;

    /**
     * Proxy port if set via configuration.
     */
    private int m_proxyPort;

    /**
     * Proxy host if set via configuration.
     */
    private String m_proxyHost;

    /**
     * Whether the data held within this instance is currently accurate.
     */
    private boolean m_dataValid;

    /**
     * Whether the resource exists on the server.
     */
    private boolean m_exists;

    /**
     * The mime type of the resource on the server.
     */
    private String m_mimeType;

    /**
     * The content length of the resource on the server.
     */
    private long m_contentLength;

    /**
     * The last modified date of the resource on the server.
     */
    private long m_lastModified;

    /**
     * Stored {@link SourceValidity} object.
     */
    private SourceValidity m_cachedValidity;

    /**
     * Cached last modification date.
     */
    private long m_cachedLastModificationDate;

    /**
     * Constructor, creates a new {@link HTTPClientSource} instance.
     *
     * @param uri URI
     * @param parameters contextual parameters passed to this instance
     * @exception Exception if an error occurs
     */
    public HTTPClientSource( final String uri, final Map parameters, final HttpState httpState )
        throws Exception
    {
        this.m_uri = uri;
        this.m_parameters = parameters == null ? Collections.EMPTY_MAP : parameters;
        this.m_httpState = httpState;
    }

    /**
     * Parameterizes this {@link HTTPClientSource} instance.
     *
     * @param params a {@link Parameters} instance.
     * @exception ParameterException if an error occurs
     */
    public void parameterize( final Parameters params )
        throws ParameterException
    {
        this.m_proxyHost = params.getParameter( PROXY_HOST, null );
        this.m_proxyPort = params.getParameterAsInteger( PROXY_PORT, -1 );

        if ( this.getLogger().isDebugEnabled() )
        {
            final String message =
                this.m_proxyHost == null || this.m_proxyPort == -1
                ? "No proxy configured"
                : "Configured with proxy host "
                  + this.m_proxyHost + " port " + this.m_proxyPort;

            this.getLogger().debug( message );
        }
    }

    /**
     * Initializes this {@link HTTPClientSource} instance.
     *
     * @exception Exception if an error occurs
     */
    public void initialize() throws Exception
    {
        this.m_client = new HttpClient();

        if ( this.m_proxyHost != null && this.m_proxyPort != -1 )
        {
            this.m_client.getHostConfiguration().setProxy( this.m_proxyHost, this.m_proxyPort );
        }
        if (this.m_httpState != null)
        {
            this.m_client.setState(this.m_httpState);
        }

        this.m_dataValid = false;
    }

    /**
     * Method to discover what kind of request is being made from the
     * parameters map passed in to this Source's constructor.
     *
     * @return the method type, or if no method type can be found,
     *         HTTP GET is assumed.
     */
    private String findMethodType()
    {
        final String method =
            (String) this.m_parameters.get( SourceResolver.METHOD );
        return method == null ? GET : method;
    }

    /**
     * Helper method to create the required {@link HttpMethod} object
     * based on parameters passed to this {@link HTTPClientSource} object.
     *
     * @return a {@link HttpMethod} object.
     */
    private HttpMethod getMethod()
    {
        final String method = this.findMethodType();

        // create a POST method if requested
        if ( POST.equals( method ) )
        {
            return this.createPostMethod(
                this.m_uri,
                (SourceParameters) this.m_parameters.get( SourceResolver.URI_PARAMETERS )
            );
        }

        // default method is GET
        return this.createGetMethod( this.m_uri );
    }

    /**
     * Factory method to create a new {@link PostMethod} with the given
     * {@link SourceParameters} object.
     *
     * @param uri URI
     * @param params {@link SourceParameters}
     * @return a {@link PostMethod} instance
     */
    private PostMethod createPostMethod(
        final String uri, final SourceParameters params
    )
    {
        final PostMethod post = new PostMethod( uri );

        if ( params == null )
        {
            return post;
        }

        for ( final Iterator names = params.getParameterNames();
              names.hasNext();
        )
        {
            final String name = (String) names.next();

            for ( final Iterator values = params.getParameterValues( name );
                  values.hasNext();
            )
            {
                final String value = (String) values.next();
                post.addParameter( new NameValuePair( name, value ) );
            }
        }

        return post;
    }

    /**
     * Factory method to create a {@link GetMethod} object.
     *
     * @param uri URI
     * @return a {@link GetMethod} instance
     */
    private GetMethod createGetMethod( final String uri )
    {
        final GetMethod method = new GetMethod( uri );

        // add all parameters as headers
        for ( final Iterator i = this.m_parameters.keySet().iterator(); i.hasNext(); )
        {
            final String key = (String) i.next();
            final String value = (String) this.m_parameters.get( key );

            if ( this.getLogger().isDebugEnabled() )
            {
                this.getLogger().debug(
                    "Adding header '" + key + "', with value '" + value + "'"
                );
            }

            method.setRequestHeader( key, value );
        }

        return method;
    }

    /**
     * Factory method to create a {@link HeadMethod} object.
     *
     * @param uri URI
     * @return a {@link HeadMethod} instance
     */
    private HeadMethod createHeadMethod( final String uri )
    {
        return new HeadMethod( uri );
    }

    /**
     * Factory method to create a {@link PutMethod} object.
     *
     * @param uri URI to upload <code>uploadFile</code> to
     * @param uploadFile {@link File} to be uploaded
     * @return a {@link PutMethod} instance
     * @exception IOException if an error occurs
     */
    private PutMethod createPutMethod(
        final String uri, final File uploadFile
    )
        throws IOException
    {
        final PutMethod put = new PutMethod( uri );
        put.setRequestEntity(new InputStreamRequestEntity(
            new FileInputStream( uploadFile.getAbsolutePath() )));
        return put;
    }

    /**
     * Factory method to create a {@link DeleteMethod} object.
     *
     * @param uri URI to delete
     * @return {@link DeleteMethod} instance.
     */
    private DeleteMethod createDeleteMethod( final String uri )
    {
        return new DeleteMethod( uri );
    }

    /**
     * Method to make response data available if possible without
     * actually making an actual request (ie. via HTTP HEAD).
     */
    private void updateData()
    {
        // no request made so far, attempt to get some response data.
        if ( !this.m_dataValid )
        {
            if ( GET.equals( this.findMethodType() ) )
            {
                final HttpMethod head = this.createHeadMethod( this.m_uri );
                try
                {
                    this.executeMethod( head );
                    return;
                }
                catch ( final IOException e )
                {
                    if ( this.getLogger().isDebugEnabled() )
                    {
                        this.getLogger().debug(
                            "Unable to determine response data, using defaults", e
                        );
                    }
                }
                finally {
                    head.releaseConnection();
                }
            }

            // default values when response data is not available
            this.m_exists = false;
            this.m_mimeType = null;
            this.m_contentLength = -1;
            this.m_lastModified = 0;
            this.m_dataValid = true;
        }
    }

    /**
     * Executes a particular {@link HttpMethod} and updates internal
     * data storage.
     *
     * @param method {@link HttpMethod} to execute
     * @return response code from server
     * @exception IOException if an error occurs
     */
    protected int executeMethod( final HttpMethod method )
        throws IOException
    {

        final int response = this.m_client.executeMethod( method );

        this.updateExists( method );
        this.updateMimeType( method );
        this.updateContentLength( method );
        this.updateLastModified( method );

        // all finished, return response code to the caller.
        return response;
    }

    /**
     * Method to update whether a referenced resource exists, after
     * executing a particular {@link HttpMethod}.
     *
     * <p>REVISIT: exists() would be better called canRead()
     * or similar, as a resource can exist but not be readable.</p>
     *
     * @param method {@link HttpMethod} executed.
     */
    private void updateExists( final HttpMethod method )
    {
        final int response = method.getStatusCode();

        // The following returns true, if the user can successfully get
        // an InputStream without receiving errors? ie. if we receive a
        // HTTP 200 (OK), 201 (CREATED), 206 (PARTIAL CONTENT)

        // REVISIT(MC): need a special way to handle 304 (NOT MODIFIED)
        // 204 & 205 in the future

        // resource does not exist if HttpClient returns a 404 or a 410
        this.m_exists = (response == HttpStatus.SC_OK ||
                    response == HttpStatus.SC_CREATED ||
                    response == HttpStatus.SC_PARTIAL_CONTENT);
    }

    /**
     * Method to ascertain whether the given resource actually exists.
     *
     * @return <code>true</code> if the resource pointed to by the
     *         URI during construction exists, <code>false</code>
     *         otherwise.
     */
    public boolean exists()
    {
        this.updateData();
        return this.m_exists;
    }

    /**
     * Method to obtain an {@link InputStream} to read the response
     * from the server.
     *
     * @return {@link InputStream} containing data sent from the server.
     * @throws IOException if some I/O problem occurs.
     * @throws SourceNotFoundException if the source doesn't exist.
     */
    public InputStream getInputStream()
        throws IOException, SourceNotFoundException
    {
        final HttpMethod method = this.getMethod();
        int response = this.executeMethod( method );
        this.m_dataValid = true;

        // throw SourceNotFoundException - according to Source API we
        // need to throw this if the source doesn't exist.
        if ( !this.exists() )
        {
            final StringBuffer error = new StringBuffer();
            error.append( "Unable to retrieve URI: " );
            error.append( this.m_uri );
            error.append( " (" );
            error.append( response );
            error.append( ")" );

            throw new SourceNotFoundException( error.toString() );
        }

        return method.getResponseBodyAsStream();
    }

    /**
     * Obtain the absolute URI this {@link Source} object references.
     *
     * @return the absolute URI this {@link String} object references.
     */
    public String getURI()
    {
        return this.m_uri;
    }

    /**
     * Return the URI scheme identifier, ie.  the part preceding the fist ':'
     * in the URI (see <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396</a>).
     *
     * @return the URI scheme identifier
     */
    public String getScheme()
    {
        return SourceUtil.getScheme( this.m_uri );
    }

    /**
     * Obtain a {@link SourceValidity} object.
     *
     * @return a {@link SourceValidity} object, or
     *         <code>null</code> if this is not possible.
     */
    public SourceValidity getValidity()
    {
        // Implementation taken from URLSource.java, Kudos :)

        final long lm = this.getLastModified();

        if ( lm > 0 )
        {
            if ( lm == this.m_cachedLastModificationDate )
            {
                return this.m_cachedValidity;
            }

            this.m_cachedLastModificationDate = lm;
            this.m_cachedValidity = new TimeStampValidity( lm );
            return this.m_cachedValidity;
        }

        return null;
    }

    /**
     * Refreshes this {@link Source} object.
     */
    public void refresh()
    {
        this.recycle();
    }

    /**
     * Method to update the mime type of a resource after
     * executing a particular {@link HttpMethod}.
     *
     * @param method {@link HttpMethod} executed
     */
    private void updateMimeType( final HttpMethod method )
    {
        // REVISIT: should this be the mime-type, or the content-type -> URLSource
        // returns the Content-Type, so we'll follow that for now.
        final Header header = method.getResponseHeader( CONTENT_TYPE );
        this.m_mimeType = header == null ? null : header.getValue();
    }

    /**
     * Obtain the mime-type for the referenced resource.
     *
     * @return mime-type for the referenced resource.
     */
    public String getMimeType()
    {
        this.updateData();
        return this.m_mimeType;
    }

    /**
     * Method to update the content length of a resource after
     * executing a particular {@link HttpMethod}.
     *
     * @param method {@link HttpMethod} executed
     */
    private void updateContentLength( final HttpMethod method )
    {
        try
        {
            final Header length =
                method.getResponseHeader( CONTENT_LENGTH );
            this.m_contentLength =
                length == null ? -1 : Long.parseLong( length.getValue() );
        }
        catch ( final NumberFormatException e )
        {
            if ( this.getLogger().isDebugEnabled() )
            {
                this.getLogger().debug(
                    "Unable to determine content length, returning -1", e
                );
            }

            this.m_contentLength = -1;
        }
    }

    /**
     * Obtain the content length of the referenced resource.
     *
     * @return content length of the referenced resource, or
     *         -1 if unknown/uncalculatable
     */
    public long getContentLength()
    {
        this.updateData();
        return this.m_contentLength;
    }

    /**
     * Method to update the last modified date of a resource after
     * executing a particular {@link HttpMethod}.
     *
     * @param method {@link HttpMethod} executed
     */
    private void updateLastModified( final HttpMethod method )
    {
        final Header lastModified = method.getResponseHeader( LAST_MODIFIED );
        try
        {
            this.m_lastModified =
                lastModified == null ? 0 : DateUtil.parseDate( lastModified.getValue() ).getTime();
        }
        catch (DateParseException e)
        {
            // we ignore this exception and simply set last modified to 0
            this.m_lastModified = 0;
        }
    }

    /**
     * Get the last modification date of this source. This date is
     * measured in milliseconds since the Epoch (00:00:00 GMT, January 1, 1970).
     *
     * @return the last modification date or <code>0</code> if unknown.
     */
    public long getLastModified()
    {
        this.updateData();
        return this.m_lastModified;
    }

    /**
     * Recycles this {@link HTTPClientSource} object so that it may be reused
     * to refresh it's content.
     */
    private void recycle()
    {
        this.m_dataValid = false;
    }

    /////////////////////////// ModifiableSource methods

    /**
     * Obtain an {@link OutputStream} to write to. The {@link OutputStream}
     * returned actually references a temporary local file, which will
     * be written to the server upon closing.
     *
     * The returned stream must be closed or cancelled by the calling code.
     *
     * @return an {@link OutputStream} instance
     * @exception IOException if an error occurs
     */
    public OutputStream getOutputStream() throws IOException
    {
        final File tempFile = File.createTempFile("httpclient", "tmp");
        return new WrappedFileOutputStream( tempFile, this.getLogger() );
    }

    /**
     * Internal class which extends {@link FileOutputStream} to
     * automatically upload the data written to it, upon a {@link #close}
     * operation.
     */
    private class WrappedFileOutputStream extends FileOutputStream
    {
        /**
         * Reference to the File being written itself.
         */
        private File m_file;

        /**
         * Reference to a {@link Logger}.
         */
        private final Logger m_logger;

        /**
         * Constructor, creates a new {@link WrappedFileOutputStream}
         * instance.
         *
         * @param file {@link File} to write to.
         * @param logger {@link Logger} reference.
         * @exception IOException if an error occurs
         */
        public WrappedFileOutputStream( final File file, final Logger logger )
            throws IOException
        {
            super( file );
            this.m_file = file;
            this.m_logger = logger;
        }

        /**
         * Closes the stream, and uploads the file written to the
         * server.
         *
         * @exception IOException if an error occurs
         */
        public void close() throws IOException
        {
            super.close();

            if ( this.m_file != null )
            {
                this.upload();
                this.m_file.delete();
                this.m_file = null;
            }
        }

        /**
         * Method to test whether this stream can be closed.
         *
         * @return <code>true</code> if possible, false otherwise.
         */
        public boolean canCancel()
        {
            return this.m_file != null;
        }

        /**
         * Cancels this stream.
         *
         * @exception IOException if stream is already closed
         */
        public void cancel() throws IOException
        {
            if ( this.m_file == null )
            {
                throw new IOException( "Stream already closed" );
            }

            super.close();
            this.m_file.delete();
            this.m_file = null;
        }

        /**
         * Helper method to attempt uploading of the local data file
         * to the remove server via a HTTP PUT.
         *
         * @exception IOException if an error occurs
         */
        private void upload()
            throws IOException
        {
            final HttpMethod uploader = HTTPClientSource.this.createPutMethod( HTTPClientSource.this.m_uri, this.m_file );

            if ( this.m_logger.isDebugEnabled() )
            {
                this.m_logger.debug( "Stream closed, writing data to " + HTTPClientSource.this.m_uri );
            }

            try
            {
                final int response = HTTPClientSource.this.executeMethod( uploader );

                if ( !this.successfulUpload( response ) )
                {
                    throw new SourceException(
                        "Write to " + HTTPClientSource.this.m_uri + " failed (" + response + ")"
                    );
                }

                if ( this.m_logger.isDebugEnabled() )
                {
                    this.m_logger.debug(
                        "Write to " + HTTPClientSource.this.m_uri + " succeeded (" + response + ")"
                    );
                }
            }
            finally
            {
                if ( uploader != null )
                {
                    uploader.releaseConnection();
                }
            }
        }

        /**
         * According to RFC2616 (HTTP 1.1) valid responses for a HTTP PUT
         * are 201 (Created), 200 (OK), and 204 (No Content).
         *
         * @param response response code from the HTTP PUT
         * @return true if upload was successful, false otherwise.
         */
        private boolean successfulUpload( final int response )
        {
            return response == HttpStatus.SC_OK
                || response == HttpStatus.SC_CREATED
                || response == HttpStatus.SC_NO_CONTENT;
        }
    }

    /**
     * Deletes the referenced resource.
     *
     * @exception SourceException if an error occurs
     */
    public void delete() throws SourceException
    {
        final DeleteMethod delete = this.createDeleteMethod( this.m_uri );
        try
        {
            final int response = this.executeMethod( delete );

            if ( !this.deleteSuccessful( response ) )
            {
                throw new SourceException(
                    "Failed to delete " + this.m_uri + " (" + response + ")"
                );
            }

            if ( this.getLogger().isDebugEnabled() )
            {
                this.getLogger().debug( this.m_uri + " deleted (" + response + ")");
            }
        }
        catch ( final IOException e )
        {
            throw new SourceException(
                "IOException thrown during delete", e
            );
        }
        finally
        {
            delete.releaseConnection();
        }
    }

    /**
     * According to RFC2616 (HTTP 1.1) valid responses for a HTTP DELETE
     * are 200 (OK), 202 (Accepted) and 204 (No Content).
     *
     * @param response response code from the HTTP PUT
     * @return true if upload was successful, false otherwise.
     */
    private boolean deleteSuccessful( final int response )
    {
        return response == HttpStatus.SC_OK
            || response == HttpStatus.SC_ACCEPTED
            || response == HttpStatus.SC_NO_CONTENT;
    }

    /**
     * Method to determine whether writing to the supplied OutputStream
     * (which must be that returned from {@link #getOutputStream()}) can
     * be cancelled
     *
     * @return true if writing to the stream can be cancelled,
     *         false otherwise
     */
    public boolean canCancel( final OutputStream stream )
    {
        // with help from FileSource, dankeschoen lads :)

        if ( stream instanceof WrappedFileOutputStream )
        {
            return ((WrappedFileOutputStream) stream).canCancel();
        }

        throw new IllegalArgumentException(
            "Output stream supplied was not created by this class"
        );
    }

    /**
     * Cancels any data sent to the {@link OutputStream} returned by
     * {@link #getOutputStream()}.
     *
     * After calling this method, the supplied {@link OutputStream}
     * should no longer be used.
     */
    public void cancel( final OutputStream stream ) throws IOException
    {
        if ( stream instanceof WrappedFileOutputStream )
        {
            ((WrappedFileOutputStream) stream).cancel();
        }
        else
        {
            throw new IllegalArgumentException(
                "Output stream supplied was not created by this class"
            );
        }
    }
}
TOP

Related Classes of org.apache.excalibur.source.impl.HTTPClientSource$WrappedFileOutputStream

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.