/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

/**
 * PROPRIETARY/CONFIDENTIAL.  Use of this product is subject to license terms.
 *
 * Copyright 2001-2002 by iPlanet/Sun Microsystems, Inc.,
 * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A.
 * All rights reserved.
 */
package com.sun.enterprise.admin.server.core.channel;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.rmi.RemoteException;
import java.rmi.ServerException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.Handler;

import com.sun.enterprise.admin.common.constant.AdminConstants;
import com.sun.enterprise.admin.common.ServerInstanceStatus;
import com.sun.enterprise.admin.common.Status;
import com.sun.enterprise.admin.event.AdminEvent;
import com.sun.enterprise.admin.event.AdminEventResult;

//i18n import
import com.sun.enterprise.util.i18n.StringManager;

/**
 * RMI client is used to send admin channel messages over RMI. 
 */
public class RMIClient implements Runnable {

    private File stubFile;
    private File seedFile;
    private long stubFileTs = 0;
    private byte[] key;
    private RemoteAdminChannel stub;
    private boolean autoRefresh;
    private long autoRefreshInterval;
    private Thread autoRefreshThread;

    /**
     * Create a new RMI client using specified stub file and use the specified
     * byte array (seed) for all calls to the remote object. To obtain an
     * instance of RMIClient please use the public method
     * <code>AdminChannel.getRMIClient()</code> which ensure that all calls to
     * a particular server instance go through the same object instance
     * (of RMIClient).
     * @param stubFile name of the RMI stub file
     * @param seedFile name of the shared secret file
     * @throws IllegalArgumentException if either stubFile or seedFile is null.
     */
    RMIClient(String stubFile, String seedFile) {
        if (stubFile == null || seedFile == null) {
            warn(CLIENT_NULLARGS_ERRCODE);
            throw new IllegalArgumentException(CLIENT_NULLARGS_ERRMSG);
        }
        this.stubFile = new File(stubFile);
        this.seedFile = new File(seedFile);
        if (this.stubFile.exists()) {
            stub = readStub();
        }
        if (AdminChannel.getClientAutoRefreshEnabled()) {
            startAutoRefreshThread(
                    AdminChannel.getClientAutoRefreshInterval());
        }
    }

    /**
     * Constructor for local mode
     */
    public RMIClient(boolean isDebug, String stubFile, String seedFile) {
	if (! isDebug) {
    		logger.setLevel(java.util.logging.Level.SEVERE);
	}
        if (stubFile == null || seedFile == null) {
            throw new IllegalArgumentException(CLIENT_NULLARGS_ERRMSG);
        }
        this.stubFile = new File(stubFile);
        this.seedFile = new File(seedFile);
        if (this.stubFile.exists()) {
            stub = readStub();
        }
    }

    /**
     * Start auto refresh thread. The auto refresh thread refreshes the remote
     * stub if the stub file has changed on the disk.
     */
    void startAutoRefreshThread(long interval) {
        if (interval <= 0) {
            throw new IllegalArgumentException(INVALID_AUTO_REFRESH_INTERVAL);
        }
        autoRefresh = true;
        autoRefreshInterval = interval;
        if (autoRefreshThread != null && autoRefreshThread.isAlive()) {
            return;
        } else {
            autoRefreshThread = new Thread(this);
            autoRefreshThread.start();
        }
    }

    /**
     * Stop auto refresh thread.
     */
    void stopAutoRefreshThread() {
        autoRefresh = false;
    }

    /**
     * Auto refresh this rmi client.
     */
    public void run() {
        while (autoRefresh) {
            try {
                Thread.sleep(autoRefreshInterval);
            } catch (InterruptedException ie) {
                warn(AUTO_REFRESH_INTR);
                autoRefresh = false;
            }
            if (autoRefresh) {
                checkServerStatus();
            }
        }
    }

    /**
     * Send specified event notification over admin channel
     */
    public AdminEventResult sendNotification(AdminEvent event) {
        // Normal handling, send the event
        boolean doRetry = true;
        AdminEventResult result = null;
        if (stub != null) {
            try {
                result = stub.sendNotification(key, event);
                doRetry = false;
            } catch (ServerException re) {
                if ((re.detail != null) &&
                        (re.detail instanceof java.lang.IllegalArgumentException
                        || re.detail instanceof java.lang.SecurityException)) {
                    doRetry = false;
                    warn(EVENT_NOTIFY_ERROR);
                    debug(re.detail);
                } else {
                    if (re.detail != null) {
                        debug(re.detail);
                    }
                    debug(re);
                }
            } catch (RemoteException re) {
                if (re.detail != null) {
                    debug(re.detail);
                }
                debug(re);
            }
        }
        if (doRetry) {
            // Normal processing did not work, try to get stub again and then
            // attempt to send the event
            boolean gotNew = checkServerStatus();
            if (stub != null && gotNew) {
                try {
                    result = stub.sendNotification(key, event);
                } catch (RemoteException re) {
                    warn(EVENT_RENOTIFY_ERROR);
                    if (re.detail != null) {
                        debug(re.detail);
                    }
                    debug(re);
                }
            }
        }
        if (result == null) {
            // Still couldn't communicate, set result appropriately
            result = new AdminEventResult(event.getSequenceNumber());
            result.setResultCode(AdminEventResult.TRANSMISSION_ERROR);
            if (stub == null) {
                result.addMessage(event.getEffectiveDestination(),
                    "Remote Stub is null");
            }
        }
        return result;
    }

    /** Determines wheter the instance with given name is alive.
     *  Really speaking, checks whether the RMI channel is responsive.
     *  The method returns immediately with the status without any retries.
     *  @return boolean indicating whether the RMIServer Object
     *  that <code> this </code> server instance is alive.
     */
    public boolean isAlive() {
        return isAlive(false);
    }

    /**
     * Determines whether the instance represented by this object is alive.
     * Unless the parameter refreshStub is true, the responsiveness of
     * instance is checked by using cached stub (if any) and if there is no
     * cached stub the method returns false.
     * @param refreshStub if true, refresh remote stub if it has changed
     * @return true if the instance represented by <code>this</code> object
     *     is responding, false otherwise.
     */
    public boolean isAlive(boolean refreshStub) {

        if (refreshStub) {
            boolean gotNew = checkServerStatus();
        }

        boolean isAlive = true;

        if (stub != null) {
            try {
                stub.pingServer(key);
            }
            catch(RemoteException re) {
                debug(re);
                isAlive = false;
            }
        }
        else {
            isAlive = false;
        }
        return ( isAlive );
    }

    /**
     * Get server instance status code. If the instance can not be contacted
     * over rmi channel then the method reports that instance is not running.
     * The return value of this method is one of the constants <code>
     * kInstanceStartingCode, kInstanceRunningCode, kInstanceStoppingCode or
     * kInstanceNotRunningCode</code> from the class <code>
     * com.sun.enterprise.admin.common.Status</code>.
     * @return an int denoting the status of server starting, running, stopping
     *     or not running.
     */
    public int getInstanceStatusCode() {
        boolean gotNew = checkServerStatus();
        int statusCode = Status.kInstanceNotRunningCode;
        if (stub != null) {
            try {
                statusCode = stub.getServerStatusCode(key);
            } catch (RemoteException re) {
                debug(CHANNEL_COMM_ERROR, stubFile.getName());
                if (re.detail != null) {
                    trace(re.detail);
                }
                trace(re);
            } catch (IllegalArgumentException iae) {
                debug(CHANNEL_COMM_ERROR, stubFile.getName());
                trace(iae);
                // This means that the key did not match. Attempt to read
                // the key from the disk again to work around the race
                // condition when the file is read on the client before the
                // server has finished writing (and hence client has partial
                // key). Another read updates the shared key on the client.
                byte[] newKey = null;
                try {
                    newKey = readSeed();
                } catch (IOException ioe) {
                    debug(FILE_READ_ERROR, seedFile.getName());
                    trace(ioe);
                }
                if (newKey != null) {
                    key = newKey;
                }
                throw iae;
            }
        }
        return statusCode;
    }

    /**
     * Get the port where the conflict occurs.
     *
     * @return A valid port number.0 If there is any error in processing
     */
    public int getConflictedPort() {
        int conflictedPort = 0;
        boolean gotNew = checkServerStatus();
        if (stub != null) {
            try {
                conflictedPort = stub.getConflictedPort(key);
            } catch (RemoteException re) {
                debug(CHANNEL_COMM_ERROR, stubFile.getName());
                if (re.detail != null) {
                    trace(re.detail);
                }
                trace(re);
            }
        }
        return conflictedPort;
    }

    /**
     * Triggers exit of the server VM. Server VM will client 
     * that got the conflicted port number calls this method. 
     */
    public void triggerServerExit() {
        boolean gotNew = checkServerStatus();
        if (stub != null) {
            try {
                stub.triggerServerExit(key);
            } catch (RemoteException re) {
                debug(CHANNEL_COMM_ERROR, stubFile.getName());
                if (re.detail != null) {
                    trace(re.detail);
                }
                trace(re);
            }
        }
    }

    /**
     * Is restart needed on the instance. The status is set by admin server and
     * instance just keeps track of it across admin server restarts. If the
     * instance has already restarted since admin server set the status, the
     * instance does not require restart (as expected).
     * @return true if the instance requires restart, false otherwise.
     */
    public boolean isRestartNeeded() {
        boolean restartNeeded = false;
        boolean gotNew = checkServerStatus();
        if (stub != null) {
            try {
                restartNeeded = stub.isRestartNeeded(key);
            } catch (RemoteException re) {
                debug(CHANNEL_COMM_ERROR, stubFile.getName());
                if (re.detail != null) {
                    trace(re.detail);
                }
                trace(re);
            }
        }
        return restartNeeded;
    }

    /**
     * Set restart needed status for a server instance. If the instance is not
     * running or if there is an error in communicating, the instance restart
     * status will not be changed. The status set by this method is preserved
     * in the instance across admin server restarts.
     * @param restartNeeded true if instance restart is needed, false otherwise.
     */
    public void setRestartNeeded(boolean restartNeeded) {
        boolean gotNew = checkServerStatus();
        if (stub != null) {
            try {
                stub.setRestartNeeded(key, restartNeeded);
            } catch (RemoteException re) {
                debug(CHANNEL_COMM_ERROR, stubFile.getName());
                if (re.detail != null) {
                    trace(re.detail);
                }
                trace(re);
            }
        }
    }

    /**
     * Check status of server stub file and refresh if needed.
     */
    private boolean checkServerStatus() {
        boolean gotNew = false;
        if (stubFile.exists()) {
            long ts = stubFile.lastModified();
            if (ts > stubFileTs) {
                if (stubFile.canRead()) {
                    RemoteAdminChannel obj = readStub();
                    if (obj != null) {
                        gotNew = true;
                        stub = obj;
                    }
                } else {
                    warn(FILE_READ_ERROR, stubFile.getName());
                }
            }
        } else {
            if (stub != null) {
                stub = null;
            }
        }
        return gotNew;
    }

    /**
     * Read stub from stub file
     */
    private RemoteAdminChannel readStub() {
        RemoteAdminChannel obj = null;
        FileInputStream fis = null;
        try {
            stubFileTs = stubFile.lastModified();
            fis = new FileInputStream(stubFile);
            ObjectInputStream ois = new ObjectInputStream(fis);
            obj = (RemoteAdminChannel)ois.readObject();
        } catch (IOException ioe) {
            warn(CLIENT_INIT_ERROR);
            debug(ioe);
        } catch (ClassNotFoundException cnfe) {
            warn(CLIENT_INIT_ERROR);
            debug(cnfe);
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException ioe) {
                }
            }
        }
        try {
            key = readSeed();
        } catch (IOException ioe) {
            warn(CLIENT_INIT_ERROR);
            debug(ioe);
            obj = null;
        }
        if (obj == null) {
            stubFileTs = 0;
        }
        return obj;
    }

    /**
     * Read shared secret from file.
     */
    private byte[] readSeed() throws IOException {
        byte[] seed = null;
        if (seedFile.exists() && seedFile.canRead()) {
            // Seed file is updated after creating stub file and therefore
            // it should only be read only if it has been modified since stub
            // file was modified. NOTE: This relies on the fact that the
            // server always (on every startup) writes seed file after stubfile.
            long seedFileTs = seedFile.lastModified();
            if (seedFileTs >= stubFileTs) {
                FileInputStream fis = null;
                try {
                    fis = new FileInputStream(seedFile);
                    seed = new byte[AdminChannel.SEED_LENGTH];
                    fis.read(seed);
                } catch (IOException ioe) {
                    warn(AdminChannel.KEY_READ_ERROR);
                    debug(ioe);
                    seed = null;
                } finally {
                    if (fis != null) {
                        try {
                            fis.close();
                        } catch (IOException ioe) {
                        }
                    }
                }
            } else {
                debug(SEED_FILE_OLDER,
                        new Long[] {new Long(seedFileTs), new Long(stubFileTs)});
            }
        } else {
            warn(AdminChannel.KEY_READ_ERROR);
            debug(FILE_READ_ERROR, seedFile.getName());
        }
        if (seed == null) {
			String msg = localStrings.getString( "admin.server.core.channel.unable_initializing_key", seedFile );
            throw new IOException( msg );
        }
        return seed;
    }

    /**
     * Has the server instance restarted since specified timestamp. If instance
     * is not running, the method will return false. The return value is
     * accurate within the timespan as specified in getClientRefreshInterval()
     * of AdminChannel -- meaning that if the time period since instance restart
     * is less than the specified timespan then the method may return false
     * instead of true.
     * @param ts Timestamp to check for
     * @return true if the instance has restarted since specified timestamp,
     *    false otherwise.
     */
    public boolean hasRestartedSince(long ts) {
        return hasRestartedSince(ts, false);
    }

    /**
     * Has the server instance restarted since specified timestamp. If instance
     * is not running, the method will return false. If refreshStub is true
     * then the method will attempt to read newer stub (if any) and will report
     * status using that. If refreshStub is false then the stub is not re-read
     * and return value is dependent on the cached stub (if any). 
     * @param ts Timestamp to check for
     * @param refreshStub if true, refresh remote stub if it has changed
     * @return true if the instance has restarted since specified timestamp,
     *    false otherwise.
     */
    public boolean hasRestartedSince(long ts, boolean refreshStub) {
        if (refreshStub) {
            boolean gotNew = checkServerStatus();
        }
        boolean restarted = false;
        if (stubFile != null) {
            if (stubFileTs > ts) {
                restarted = true;
            }
        }
        return restarted;
    }

    //
    // logger methods copied from AdminChannel
    //

    static void trace(Throwable t) {
        logger.log(Level.FINEST, t.getMessage(), t);
    }

    static void warn(String s) {
        logger.warning(s);
    }

    static void warn(String msgkey, String obj1) {
        logger.log(Level.WARNING, msgkey, obj1);
    }

    static void debug(String s) {
        logger.fine(s);
    }

    static void debug(String msgkey, String obj1) {
        logger.log(Level.FINE, msgkey, obj1);
    }

    static void debug(String msgkey, Object[] objarr) {
        logger.log(Level.FINE, msgkey, objarr);
    }

    static void debug(Throwable t) {
        logger.log(Level.FINE, t.getMessage(), t);
    }

	// i18n StringManager
	private static StringManager localStrings =
		StringManager.getManager( RMIClient.class );

    private static final String CLIENT_NULLARGS_ERRCODE =
            "channel.client_nullargs";
    private static final String CLIENT_NULLARGS_ERRMSG =
			localStrings.getString( "admin.server.core.channel.attempt_initializing_channel_client_with_null_arguments" );
    private static final String CLIENT_INIT_ERROR = "channel.client_init_error";
    private static final String EVENT_NOTIFY_ERROR =
            "channel.event_notify_error";
    private static final String EVENT_RENOTIFY_ERROR =
            "channel.event_renotify_error";
    private static final String AUTO_REFRESH_INTR = "channel.auto_refresh_intr";
    private static final String CHANNEL_COMM_ERROR = "channel.comm_error";
    private static final String INVALID_AUTO_REFRESH_INTERVAL =
            localStrings.getString(
                    "admin.server.core.channel.invalid_auto_refresh_interval");

    static final String FILE_READ_ERROR = "channel.file_read_error";
    static final String SEED_FILE_OLDER = "channel.seed_file_older";

    static Logger logger = Logger.getLogger(AdminConstants.kLoggerName);
}
