/*
 ************************************************************************
 *
 * Copyright (c) 2003-2004, C&C Research Laboratories, NEC Europe Ltd.
 *
 * Copyright in this software belongs to C&C Research Laboratories,
 * Rathausallee 10, 53757 Sankt Augustin, Germany.
 *
 * This software may not be used, sold, licensed, transferred, copied
 * or reproduced in whole or in part in any manner or form or in or
 * on any media by any person other than in accordance with the terms
 * of the Licence Agreement supplied with the software, or otherwise
 * without the prior written consent of the copyright owners.
 *
 * This software is distributed WITHOUT ANY WARRANTY, without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE, except where stated in the Licence Agreement supplied with
 * the software.
 *
 *  Created By :           G.A. Kohring
 *  Created for Project :  GEMSS (IST-2001-37153)
 *
 ************************************************************************
 *
 */

package de.nece.ccrle.docmonitor;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Properties;
import java.util.Vector;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Logger;
import java.util.logging.LogManager;
import java.util.logging.SimpleFormatter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

import de.nece.ccrle.util.VariableProperties;

/**
 * A class for monitoring remote documents. 
 * <p>
 * This class can be used to monitor remote documents for any changes
 * and then download them automatically when they change. It is configured
 * by setting parameters in the <code>config/docmonitor.config</code> file
 * located in the docmonitor's home directory, which in turn is specified by
 * the <tt>docmonitor.home</tt> system variable. The format of this file is
 * that of typical properties file, i.e., a list of 
 * <code>name=variable</code> pairs.
 * <p>
 * The general properties are:
 * <dl>
 *      <dt><tt>docmonitor.period=0</tt></dt><p>
 *      <dd>The Period between updates (in seconds). If the period is set to 0, 
 *      then docmonitor is run only once.</dd><p>
 *      <dt><tt>docmonitor.sslTrustLevel=system</tt></dt><p>
 *      <dd>Possible values for the SSL trust level are: <p>
 *          <dl>
 *          <dt><tt>naive</tt></dt> <p>
 *          <dd> trust all servers with a valid server certificate
 *          </dd> <p>
 *          <dt><tt>system</tt></dt> <p>
 *          <dd> trust only those servers with a servier certicate signed
 *             by a CA from the default keystore
 *          </dd> <p>
 *          <dt><tt>userStore</tt></dt> <p>
 *          <dd> trust those servers with a servier certicate signed by
 *             a CA from the default keystore or from the user supplied
 *             keystore. <p>
 *             When this value is selected the following properties must be
 *             set:
 *              <dl>
 *                  <dt><tt>docmonitor.keystore=${user.home}${/}keystore.jks
 *                      </tt></dt> <p>
 *                  <dd>The user's keystore</dd> <p>
 *                  <dt><tt>docmonitor.keystore.type=jks</tt></dt><p>
 *                  <dd>The keystore type</dd> <p>
 *                  <dt><tt>docmonitor.keystore.password=changeit</tt></dt> <p>
 *                  <dd>The keystore password</dd> <p>
 *              </dl>
 *          </dd>
 *          </dl>
 *      </dd>
 * </dl>
 * Following the general properties, are a sets of document specific
 * properties, with one set for each document.
 * <dl>
 *      <dt><tt>doc1.url=http://my.site.com:80/important.doc</tt></dt><p>
 *      <dd> url of remote document </dd> <p>
 *      <dt><tt>doc1.localFilename=${user.home}${/}mycopy.doc</tt></dt><p>
 *      <dd>name of the local copy</dd> <p>
 *      <dt><tt>doc1.authentication=none</tt></dt><p>
 *      <dd>authentication scheme for remote url.
 *      Possible values for the authentication scheme are: <p>
 *          <dl>
 *          <dt><tt>none</tt></dt> <p>
 *          <dd>No authorization required</dd><p>
 *          <dt><tt>BasicAuthentication</tt></dt> <p>
 *          <dd>HTTP BasicAuthentication</dd><p>
 *          </dl>
 *      </dd>
 *      <dt><tt>doc1.procedure=update</tt></dt><p>
 *      <dd>Procedure for updating the local copy. Possible values are:<p>
 *          <dl>
 *          <dt><tt>update</tt></dt> <p>
 *          <dd>The local document is replaced by the remote document if the
 *          remote document is newer than the local document.</dd><p>
 *          <dt><tt>replace</tt></dt> <p>
 *          <dd>The local document is always replaced by the 
 *          remote document and the modification time is not checked.</dd><p>
 *          </dl>
 *      </dd> <p>
 * </dl><p>
 * Settings for different documents are distinguished by incrementing the
 * identifier, i.e., <code>doc1</code>, <code>doc2</code>, <code>doc3</code>,
 * etc.
 * <p>
 * <code>DocMonitor</code>'s {@link #main(String[] args)} method can be used
 * to run this as a stand-alone application.
 *
 * @author Greg Kohring
 */
public class DocMonitor extends TimerTask
{
    private Properties properties = null;
    private Vector docAtts = null;
    private String prefix = "doc";
    private DocRetriever retriever = null;
    private File workDir = null;
    private long lastCheck = 0;
    private static Logger theLogger = 
                    Logger.getLogger( DocMonitor.class.getName() );

    /**
     * Creates a new document monitor. 
     *<p>
     * When a new monitor is created it
     * reads in the configuration paramaters from the properties file
     * described above.
     */
    public DocMonitor() {

        // Load the properties file and initialize the monitor

        properties = loadProperties();

        init();

    }

    // Load the properties file for this monitor
    private Properties loadProperties(){
        // Get the monitor's home

        String homeDirectory = System.getProperty( "docmonitor.home" );

        // If monitor's home is not set, then abort.

        if ( homeDirectory == null ){
            throw new RuntimeException( "docmonitor.home == null!" );
        }

        Properties properties = new VariableProperties ();

        // Locate the configuration files

        properties.setProperty( "docmonitor.home", homeDirectory );

        String config = homeDirectory + File.separator + "config" 
                            + File.separator + "docmonitor.config";

        // Read in the configuration information

        try {
            FileInputStream fis = new FileInputStream( config );
            properties.load( fis );
            fis.close();
        } catch ( FileNotFoundException fnfe ){
            throw new RuntimeException( fnfe.getLocalizedMessage() );
        } catch ( IOException ioe ){
            throw new RuntimeException( ioe.getLocalizedMessage() );
        }

        return properties;
    }

    // Initialize the monitor
    private void init(){

        // Create a retriever object 

        retriever = new DocRetriever( 
                       properties.getProperty( "docmonitor.sslTrustLevel" ),
                       properties.getProperty( "docmonitor.keystore" ),
                       properties.getProperty( "docmonitor.keystore.type" ),
                       properties.getProperty( "docmonitor.keystore.password" )
                                    );

        docAtts = new Vector();

        // Create an attributes object for each document to be monitored

        String userDir = System.getProperty( "user.dir" );

        int prop = 1;
        String propPrefix = prefix + String.valueOf( prop );
        String urlProperty = propPrefix + ".url";
        String url = null;
        while( ( url = properties.getProperty( urlProperty ) ) != null ){
            URL docURL = null;
            try{
                docURL = new URL( url );
            } catch ( MalformedURLException murle ){
                throw new RuntimeException( murle.getLocalizedMessage() );
            }
            String filename = properties.getProperty( propPrefix + 
                                                        ".localFilename" );

            filename = configurePath( filename, userDir );

            File docFile = new File( filename );
            DocAttributes docAtt = new DocAttributes( docURL, docFile,
                    properties.getProperty( propPrefix + ".procedure" ),
                    properties.getProperty( propPrefix + ".authentication" ),
                    properties.getProperty( propPrefix + ".user" ),
                    properties.getProperty( propPrefix + ".password" ) );

            // At this point we really should check whether or not the
            // password has been set and if not we need to ask the user to
            // supply it.

            docAtts.add( docAtt );

            prop++;
            propPrefix = prefix + String.valueOf( prop );
            urlProperty = propPrefix + ".url";
        }

    }

    /**
     * Retrieves the home directory for this monitor.  The home directory is
     * set in the monitors properties file.
     *
     * @return a <code>String</code> containing the name of this monitor's
     *      home directory.
     */
    public String getMonitorHome(){
        return System.getProperty( "docmonitor.home" ); 
    }

    /**
     * Retrieves the period between monitoring events. 
     *
     * @return a period of time, in seconds, between 2 consecutive attempts
     *      to update a document.
     */
    public long getPeriod(){
        String period = properties.getProperty( "docmonitor.period" );
        if ( period == null ){
            return -1;
        } else {
            return Long.parseLong( period ) * 1000;
        }
    }

    /**
     * Retrieves the logger for this class.
     *
     * @return the class {@link java.util.logging.Logger}.
     */
    public Logger getLogger(){
        return theLogger;
    }

    /**
     * This is the method which actually does all the work. It iterates over
     * all the documents in the config file, processing them as described
     * above. 
     */
    public void run(){

        //Get the logger

        Logger theLogger = Logger.getLogger( DocMonitor.class.getName() );

        for ( Iterator docIt = docAtts.iterator(); docIt.hasNext() ; ){

            //Get the document attributes

            DocAttributes docAtt = (DocAttributes) docIt.next();   

            //Create a temporary file to hold the contents of the remote
            //document

            String tmpPrefix = "dmd";

            File localFile = docAtt.getLocalFile();

            cleanTempFiles( localFile, tmpPrefix, ".tmp" );

            File tempFile = null;
            try{
                tempFile = File.createTempFile( tmpPrefix, null,
                        localFile.getAbsoluteFile().getParentFile() );
                tempFile.deleteOnExit();
            } catch ( IOException ioe ){
                theLogger.severe( localFile + ": " + 
                                            ioe.getLocalizedMessage() );
                continue;
            }

            //Open the temporary file for input

            FileOutputStream fos = null;
            try{
                fos = new FileOutputStream( tempFile );
            } catch ( FileNotFoundException fnfe ){
                theLogger.severe( tempFile + ": " + 
                                            fnfe.getLocalizedMessage() );
                continue;
            }

            BufferedOutputStream bos = new BufferedOutputStream( fos );

            theLogger.info( "Querying " + docAtt.getURL().toString() );

            //Start the retrieval processes

            try{
                boolean downloaded = false;

                if ( docAtt.getProcedure().equals( "update" ) ){
                    downloaded = retriever.retrieve( docAtt, bos,
                                            localFile.lastModified() );
                } else {
                    downloaded = retriever.retrieve( docAtt, bos );
                }

                //We need to close the output stream before we can move
                //the temp file on a windows system...
                bos.close();

                //If the downloaded was successful then replace the local file
                //by the new file. We do this in a two step process to make
                //sure the local file is never lost.

                if ( downloaded ){

                    theLogger.info( "Downloading: " + 
                                docAtt.getURL() + " to " + tempFile );

                    File localTempFile = new File( localFile.toString() + 
                                                                ".old" );
                    bos.close();
                    localFile.renameTo( localTempFile );
                    if ( !tempFile.renameTo( localFile ) ){
                        localTempFile.renameTo( localFile );
                        theLogger.severe( "Failed to replace " + localFile + 
                                  " with " + tempFile );
                    } else {
                        localTempFile.delete();
                        theLogger.info( "File, " + localFile + 
                                  ", replaced by " + tempFile );
                    }
                } else {
                    theLogger.info( "File, " + localFile + ", unchanged.");
                }
            } catch ( IOException ioe ){
                ioe.printStackTrace();
                theLogger.severe( ioe.getLocalizedMessage() );    
            } catch ( UnsupportedAuthSchemeException uase ){
                uase.printStackTrace();
                theLogger.severe( uase.getLocalizedMessage() );    
            } finally {
                tempFile.delete();
            }

        }

        // Flush any log messages

        Handler[] handlers = theLogger.getHandlers();
        for ( int h = 0; h < handlers.length; h++ ){
            handlers[h].flush();
        }
    }

    /**
     * Finalizes the object by closing any open files.
     */
    protected void finalize() throws Throwable{

        // Close all the handlers

        Logger theLogger = Logger.getLogger( DocMonitor.class.getName() );

        Handler[] handlers = theLogger.getHandlers();
        for ( int h = 0; h < handlers.length; h++ ){
            handlers[h].close();
        }
    }

    private String configurePath( String relPath, String base ){

        if ( relPath ==  null ){
            throw new IllegalArgumentException( "relPath == null!" );
        }

        if ( relPath.startsWith( File.separator ) ){
            return relPath;
        } else if ( relPath.startsWith( "." ) ){
            return relPath;
        } else if ( relPath.charAt( 1 ) == ':' ){
            return relPath;
        } else {
            if ( ( base != null ) && ( !base.equals( "" ) ) ) {
                String path = base + File.separator + relPath;
                return path;
            } else {
                return relPath;
            }
        }

    }

    /**
     * The <code>main</code> method can be used to run <code>DocMonitor</code>
     * as a stand-alone application.
     *
     * @param args an array of <code>String</code>s containing the command
     *      line arguments.
     */
	public static void main(String[] args)
	{
        // Create the URLMonitor
        DocMonitor dm = new DocMonitor();

        // Set the logger handler

        FileHandler fh = null;

        String dmHome = dm.getMonitorHome();

        try{
            fh = new FileHandler( dmHome + "/monitor_log.%g.%u", 100000, 
                                                                30, true );
        } catch ( IOException ioe ){
            throw new RuntimeException( ioe.getLocalizedMessage() );
        }

        fh.setFormatter( new SimpleFormatter() );

        dm.getLogger().addHandler( fh );
        dm.getLogger().setUseParentHandlers( false );

        // Start the docMonitor

        if ( dm.getPeriod() > 0 ) {
        // Start the updating thread.
            Timer timer = new Timer();
            timer.schedule( dm, 0, dm.getPeriod() );
        } else {
        // Just run the monitor once.
            dm.run();
        }

	}

   private void cleanTempFiles( final File file, final String prefix,
                                final String suffix ){

        File dir = file.getAbsoluteFile().getParentFile();

        if ( dir == null ) return;

        File tempFiles[] = dir.listFiles( new FileFilter(){
            public boolean accept( File file ){
                String name = file.getName();
                if ( name.startsWith( prefix ) ) {
                    return name.endsWith( suffix ) ;
                } else {
                    return false;
                }
            }
        } );

        for ( int f = 0; f < tempFiles.length; f++ ){
            tempFiles[f].delete();
        }

    }


}
