/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2005 Phex Development Group
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 *  --- CVS Information ---
 *  $Id: GWebCacheConnection.java,v 1.20 2005/11/13 10:28:50 gregork Exp $
 */
package phex.gwebcache;

import java.io.*;
import java.net.*;
import java.util.ArrayList;

import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;

import phex.common.Environment;
import phex.common.ServiceManager;
import phex.common.address.*;
import phex.connection.ProtocolNotSupportedException;
import phex.http.HTTPHeaderNames;
import phex.net.presentation.PresentationManager;
import phex.security.PhexSecurityManager;
import phex.utils.*;

/**
 * This class implements the basic functionality of the Gnutella Web Cache (GWebCache)
 * invented from the Gnucleus Team.
 *
 * To access a GWebCache you should send a ping request first. To verify if it is
 * working.
 *
 * Supports Version 1.3 of GWebCache specification.
 *
 * @author Gregor Koukkoullis (Phex Development Team) (C) 2002
 * @version 2002/05/27
 */
public class GWebCacheConnection
{
    private static final String PING_QUERY = "ping=1";
    private static final String HOST_FILE_QUERY = "hostfile=1";
    private static final String URL_FILE_QUERY = "urlfile=1";
    private static final String IP_QUERY = "ip=";
    private static final String URL_QUERY = "url=";

    private static final String QUERY_POSTFIX =
        "&client=PHEX&version=" + VersionUtils.getFullProgramVersion();

    /**
     * The GWebCache base URL.
     */
    private GWebCache gWebCache;

    /**
     * The connection to the GWebCache. The connection is opened when accessing
     * the GWebCache the first time.
     */
    private HttpMethod method;

    /**
     * The reader of the connection.
     */
    private BufferedReader reader;

    /**
     * Indicates if a cache is bad.
     */
    boolean isCacheBad;

    /**
     * @param aGWebCacheURL the URL to the GWebCache. Must be http protocol.
     */
    public GWebCacheConnection( GWebCache aGWebCache )
        throws ProtocolNotSupportedException
    {
        if ( !aGWebCache.getUrl().getProtocol().equals( "http" ) )
        {
            throw new ProtocolNotSupportedException(
                "Only http URLs are supported for a GWebCacheConnection" );
        }
        this.gWebCache = aGWebCache;
        isCacheBad = false;
    }

    public GWebCache getGWebCache()
    {
        return gWebCache;
    }

    /**
     * Sends a ping request to the GWebCache. The method returns true if a PONG
     * was received otherwise it returns false and set the cache to be a bad cache.
     */
    public boolean sendPingRequest()
    {
        try
        {
            URL requestURL = new URL( gWebCache.getUrl(), gWebCache.getUrl().getPath()
                + '?' + PING_QUERY + QUERY_POSTFIX );
            if ( requestURL.getHost() == null )
            {
                NLogger.warn( NLoggerNames.GWEBCACHE, "Host is null" );
                isCacheBad = true;
                return false;
            }
            openConnection( requestURL );
            String line = reader.readLine();
            if ( line != null && line.startsWith( "PONG" ) )
            {
                return true;
            }
            else
            {
                isCacheBad = true;
                return false;
            }
        }
        catch ( UnknownHostException exp )
        {
            Logger.logMessage( Logger.FINEST, Logger.NETWORK, exp );
            isCacheBad = true;
            return false;
        }
        catch ( IOException exp )
        {
            Logger.logMessage( Logger.FINER, Logger.NETWORK, exp );
            isCacheBad = true;
            return false;
        }
        finally
        {
            closeConnection();
        }
    }

    /**
     * Request a list of hosts from the GWebCache. The returned list is either
     * null if a problem occured, empty if the GWebCache does not have any hosts
     * stored, or filled with IPs.
     */
    public DestAddress[] sendHostFileRequest()
    {
        try
        {
            URL requestURL = new URL( gWebCache.getUrl(), gWebCache.getUrl().getPath()
                + '?' + HOST_FILE_QUERY + QUERY_POSTFIX );
            if ( requestURL.getHost() == null )
            {
                NLogger.warn( NLoggerNames.GWEBCACHE, "Host is null" );
                isCacheBad = true;
                return null;
            }
            openConnection( requestURL );
            String line = reader.readLine();
            // the first line might contain an error
            if ( line != null && line.startsWith( "ERROR" ) )
            {
                isCacheBad = true;
                return null;
            }
            
            DestAddress destAddress;
            boolean falseHostFound = false;
            ArrayList hostFileList = new ArrayList( 20 );
            PhexSecurityManager securityManager = PhexSecurityManager.getInstance();
            PresentationManager presentationMgr = PresentationManager.getInstance();
            while ( line != null )
            {
                try
                {
                    // The line requires to have a valid port. If no port could
                    // be found the default port of -1 should ensure the
                    // DestAddress is invalid when no port is given.
                    destAddress = presentationMgr.createHostAddress( line, -1 );
                    if ( !destAddress.isValidAddress() )
                    {
                        throw new MalformedDestAddressException( "Invalid address." );
                    }
                    
                    byte access = securityManager.controlHostAddressAccess( 
                        destAddress );
                    switch ( access )
                    {
                        case PhexSecurityManager.ACCESS_DENIED:
                        case PhexSecurityManager.ACCESS_STRONGLY_DENIED:
                            // skip host address...
                            line = reader.readLine();
                            continue;
                    }
                    hostFileList.add( destAddress );
                }
                catch (MalformedDestAddressException exp)
                {
                    NLogger.debug( NLoggerNames.GWEBCACHE, "Invalid host found: " + line );
                    falseHostFound = true;
                    line = reader.readLine();
                    continue;
                }
                line = reader.readLine();
            }
            // all returned hosts where false, cache is bad.
            if ( hostFileList.size() == 0 && falseHostFound )
            {
                isCacheBad = true;
                return null;
            }
            DestAddress[] hostFileArr = new DestAddress[ hostFileList.size() ];
            hostFileList.toArray( hostFileArr );
            NLogger.debug( NLoggerNames.GWEBCACHE, "Hosts found: " + hostFileArr.length );
            return hostFileArr;
        }
        catch ( IOException exp )
        {
            Logger.logMessage( Logger.FINER, Logger.NETWORK, exp );
            isCacheBad = true;
            return null;
        }
        finally
        {
            closeConnection();
        }
    }

    /**
     * Request a list of GWebCache URLs from the GWebCache. The returned list is
     * either null if a problem occured, empty if the GWebCache does not have any
     * URLs stored, or filled with URLs.
     */
    public URL[] sendURLFileRequest()
    {
        try
        {
            URL requestURL = new URL( gWebCache.getUrl(), gWebCache.getUrl().getPath()
                + '?' + URL_FILE_QUERY + QUERY_POSTFIX );
            if ( requestURL.getHost() == null )
            {
                NLogger.warn( NLoggerNames.GWEBCACHE, "Host is null" );
                isCacheBad = true;
                return null;
            }
            openConnection( requestURL );
            String line = reader.readLine();
            // the first line might contain an error
            if ( line != null && line.startsWith( "ERROR" ) )
            {
                isCacheBad = true;
                return null;
            }

            boolean falseURLFound = false;
            ArrayList urlFileList = new ArrayList( 20 );
            int counter = 0;
            // dont apply more then 20 URL
            while ( line != null && counter < 20 )
            {
                try
                {
                    NormalizableURL helpUrl = new NormalizableURL( line );
                    helpUrl.normalize();
                    URL url = new URL( helpUrl.toExternalForm() );
                    if ( !url.getProtocol().equals( "http" ) )
                    {
                        throw new ProtocolNotSupportedException(
                            "Only http URLs are supported for a GWebCacheConnection" );
                    }
                    urlFileList.add( url );
                }
                catch ( MalformedURLException exp )
                {//ignore false url
                    falseURLFound = true;
                }
                line = reader.readLine();
                counter ++;
            }
            // all returned urls where false, cache is bad.
            if ( urlFileList.size() == 0 && falseURLFound )
            {
                isCacheBad = true;
                return null;
            }

            URL[] urlFileArr = new URL[ urlFileList.size() ];
            urlFileList.toArray( urlFileArr );
            NLogger.debug( NLoggerNames.GWEBCACHE, "URLs found: " + urlFileArr.length );
            return urlFileArr;
        }
        catch ( IOException exp )
        {
            Logger.logMessage( Logger.FINER, Logger.NETWORK, exp );
            isCacheBad = true;
            return null;
        }
        finally
        {
            closeConnection();
        }
    }

    /**
     * Sends a update request to update the GWebCache. You can provide a fullHostName
     * and a cacheURL or only one of them by settings the other one to null.
     */
    public boolean updateRequest( String fullHostName, String cacheURL )
    {
        if ( fullHostName == null && cacheURL == null )
        {
            throw new IllegalArgumentException(
                "Must provide at least one of hostIP or cacheURL." );
        }

        try
        {
            StringBuffer queryBuffer = new StringBuffer();
            if ( fullHostName != null )
            {
                queryBuffer.append( IP_QUERY );
                queryBuffer.append( URLCodecUtils.encodeURL( fullHostName ) );
            }
            if ( cacheURL != null )
            {
                if ( fullHostName != null )
                {
                    queryBuffer.append( '&' );
                }
                queryBuffer.append( URL_QUERY );
                queryBuffer.append( URLCodecUtils.encodeURL( cacheURL ) );
            }

            URL requestURL = new URL( gWebCache.getUrl(), gWebCache.getUrl().getPath()
                + '?' + queryBuffer.toString() + QUERY_POSTFIX );
            if ( requestURL.getHost() == null )
            {
                NLogger.warn( NLoggerNames.GWEBCACHE, "Host is null" );
                isCacheBad = true;
                return false;
            }
            openConnection( requestURL );
            String line = reader.readLine();
            if ( line != null && line.startsWith( "OK" ) )
            {
                return true;
            }
            else
            {
                isCacheBad = true;
                return false;
            }
        }
        catch ( IOException exp )
        {
            Logger.logMessage( Logger.FINER, Logger.NETWORK, exp );
            isCacheBad = true;
            return false;
        }
        finally
        {
            closeConnection();
        }
    }

    /**
     * Returns if a cache is bad.
     *
     * Bad caches are those that return:<br>
     * - nothing - those that cannot be accessed at all (timeouts, invalid hostnames, etc.)<br>
     * - HTTP error codes (400-599)<br>
     * - responses that cannot be parsed by a client<br>
     * - ERROR response<p>
     */
    public boolean isCacheBad()
    {
        return isCacheBad;
    }

    /**
     * Opens a connection to the request url and checks the response code.
     * 3xx are automaticly redirected.
     * 400 - 599 response codes will throw a ConnectException.
     */
    private void openConnection( URL requestURL )
        throws IOException
    {
        HttpClient client = new HttpClient();
        client.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, 
            new DefaultHttpMethodRetryHandler( 0, false ) );
        if (   ServiceManager.sCfg.isHttpProxyUsed 
            && !StringUtils.isEmpty(ServiceManager.sCfg.httpProxyHost ) )
        {
            client.getHostConfiguration().setProxy(ServiceManager.sCfg.httpProxyHost, 
                ServiceManager.sCfg.httpProxyPort );
        }
            
        method = new GetMethod( requestURL.toExternalForm() );
        method.setFollowRedirects(false);
        method.addRequestHeader("Cache-Control", "no-cache");
        // be HTTP/1.1 complient
        method.addRequestHeader( HTTPHeaderNames.USER_AGENT,
            Environment.getPhexVendor() );
        method.addRequestHeader( HTTPHeaderNames.CONNECTION,
            "close" );
        
        NLogger.debug(NLoggerNames.GWEBCACHE, "Open GWebCache connection to " +
            requestURL + "." );
        int responseCode = client.executeMethod(method);      
        NLogger.debug(NLoggerNames.GWEBCACHE, "GWebCache " +
            requestURL + " returned response code: " + responseCode );
        
        // we only accept the status codes 2xx all others fail...
        if ( responseCode < 200 && responseCode > 299 )
        {
            throw new ConnectException( "GWebCache service not available, response code: "
                + responseCode );
        }
        
        InputStream bodyStream = method.getResponseBodyAsStream();
        if ( bodyStream != null )
        {
            reader = new BufferedReader( new InputStreamReader(
                bodyStream ) );
        }
        else
        {
            throw new ConnectException( "Empty response." );
        }
    }
    
    private void closeConnection()
    {
        
        IOUtil.closeQuietly( reader );
        reader = null;
        
        method.releaseConnection();
        
        NLogger.debug( NLoggerNames.GWEBCACHE, "Releasing HTTP method." );
    }
}