/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2004 Gregor Koukkoullis ( phex <at> kouk <dot> de )
 *
 *  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.5 2004/09/28 16:49:12 gregork Exp $
 */
package phex.gwebcache;

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

import phex.common.Environment;
import phex.connection.ProtocolNotSupportedException;
import phex.host.HostAddress;
import phex.http.HTTPHeaderNames;
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.getProgramVersion();

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

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

    /**
     * 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( URL aGWebCacheURL )
        throws ProtocolNotSupportedException
    {
        if ( !aGWebCacheURL.getProtocol().equals( "http" ) )
        {
            throw new ProtocolNotSupportedException(
                "Only http URLs are supported for a GWebCacheConnection" );
        }
        gWebCacheURL = aGWebCacheURL;
        isCacheBad = false;
    }

    public URL getGWebCacheURL()
    {
        return gWebCacheURL;
    }

    /**
     * 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( gWebCacheURL, gWebCacheURL.getPath()
                + '?' + PING_QUERY + QUERY_POSTFIX );
            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 HostAddress[] sendHostFileRequest()
    {
        try
        {
            URL requestURL = new URL( gWebCacheURL, gWebCacheURL.getPath()
                + '?' + HOST_FILE_QUERY + QUERY_POSTFIX );
            openConnection( requestURL );

            String line = reader.readLine();
            // the first line might contain an error
            if ( line != null && line.startsWith( "ERROR" ) )
            {
                isCacheBad = true;
                return null;
            }
            boolean falseHostFound = false;
            ArrayList hostFileList = new ArrayList( 20 );
            PhexSecurityManager securityManager = PhexSecurityManager.getInstance();
            while ( line != null )
            {
                byte[] ip = HostAddress.parseIP( line );
                if ( ip == null )
                {
                    line = reader.readLine();
                    falseHostFound = true;
                    continue;
                }
                byte access = securityManager.controlHostIPAccess( ip );
                switch ( access )
                {
                    case PhexSecurityManager.ACCESS_DENIED:
                    case PhexSecurityManager.ACCESS_STRONGLY_DENIED:
                        // skip host address...
                        line = reader.readLine();
                        continue;
                }

                int port = HostAddress.parsePort( line );
                if ( port != -1 )
                {
                    HostAddress address = new HostAddress( ip, port );
                    hostFileList.add( address );
                }
                else
                {
                    falseHostFound = true;
                }
                line = reader.readLine();
            }
            // all returned hosts where false, cache is bad.
            if ( hostFileList.size() == 0 && falseHostFound )
            {
                isCacheBad = true;
                return null;
            }
            HostAddress[] hostFileArr = new HostAddress[ hostFileList.size() ];
            hostFileList.toArray( hostFileArr );
            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( gWebCacheURL, gWebCacheURL.getPath()
                + '?' + URL_FILE_QUERY + QUERY_POSTFIX );
            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 );
            while ( line != null )
            {
                try
                {
                    URL url = new URL( line );
                    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();
            }
            // 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 );
            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( gWebCacheURL, gWebCacheURL.getPath()
                + '?' + queryBuffer.toString() + QUERY_POSTFIX );
            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
    {
        connection = (HttpURLConnection)requestURL.openConnection();
        connection.setUseCaches( false );
        // dont follow http redirects.
        connection.setInstanceFollowRedirects( false );
        // tell a proxy to let me through
        connection.setRequestProperty( "Cache-Control", "no-cache" );
        // be HTTP/1.1 complient
        connection.setRequestProperty( HTTPHeaderNames.USER_AGENT,
            Environment.getPhexVendor() );
        connection.setRequestProperty( HTTPHeaderNames.CONNECTION,
            "close" );

        try
        {
            reader = new BufferedReader( new InputStreamReader(
                connection.getInputStream() ) );
        }
        catch ( FileNotFoundException exp )
        {// connection was good but returned responseCode >= 400 with no file.
            throw new ConnectException( "GWebCache service not available, FileNotFound." );
        }
        int responseCode = connection.getResponseCode( );
        Logger.logMessage( Logger.FINE, Logger.NETWORK, "GWebCache connection to " +
            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 );
        }
    }
    
    private void closeConnection()
    {
        IOUtil.closeQuietly( reader );
        reader = null;

        if ( connection != null )
        {
            connection.disconnect();
            connection = null;
        }
    }
}