/*
 * 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.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress;
import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;
import java.rmi.server.RemoteStub;
import java.rmi.server.UnicastRemoteObject;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.sun.enterprise.admin.common.constant.AdminConstants;
import com.sun.enterprise.admin.event.AdminEventResult;
import com.sun.enterprise.server.Constants;
import com.sun.enterprise.instance.ServerManager;
import com.sun.appserv.server.ServerLifecycleException;

import com.sun.enterprise.util.SystemPropertyConstants;

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

/**
 * Admin channel is used for communication between admin service agents
 * running in different server instances.
 */
public class AdminChannel {

    /**
     * A reference to logger object
     */
    static Logger logger = Logger.getLogger(AdminConstants.kLoggerName);

    static String instanceRoot = null;
    
    static final String fileSeparator = "/";

    static final int SEED_LENGTH = 16;

    private static AdminChannelServer server = null;

    private static HashMap rmiClientMap = new HashMap();

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

    /**
     * Create a RMI channel. This method creates a server object and exposes
     * the stub on local filesystem.
     */
    public static void createRMIChannel() throws ServerLifecycleException {
        try {
            server = createServerObject();
            saveStubToFile(server.getRemoteStub());
        } catch (Exception e) {
            warn(SERVER_CREATION_ERRCODE);
            debug(e);
            throw new ServerLifecycleException(e);
        }
    }

    /**
     * Remove RMI channel. Remove the server object from JVM (do not accept
     * any more calls and abort in process calls) and clean up the stub
     * exposed on filesystem.
     */
    public static void destroyRMIChannel() throws ServerLifecycleException {
        if (server != null) {
            server.setChannelStopping();
            try {
                UnicastRemoteObject.unexportObject(server, true);
            } catch (NoSuchObjectException nsoe) {
                throw new ServerLifecycleException(nsoe);
            }
        }
        deleteStubFile();
    }

    /**
     * Create a shared secret. The shared secret is saved on the filesystem
     * and is verified during every call on admin channel.
     */
    public static void createSharedSecret() throws ServerLifecycleException {        
        assertAdminServerChannelNotNull();
        String fileName = getSeedFileName();
        File seedFile = new File(fileName);
        byte[] prevSeed = getPreviousSeed(seedFile);
        SecureRandom sr = new SecureRandom(prevSeed);
        byte[] seed = new byte[SEED_LENGTH];
        sr.nextBytes(seed);
        saveSeedToFile(seed, seedFile);
        server.setSharedInfo(seed);
        server.setChannelStarting();
    }

    /**
     * Enable reconfiguration of Sun ONE Web Server core. 
     */
    public static void enableWebCoreReconfig() {
        try {
            ReconfigHelper.enableWebCoreReconfig();
        } catch (Throwable t) {
            // If reconfiguration could not be enabled log a warning
            // message and continue
            warn(RECONFIG_ENABLE_ERROR );
            debug(t);
        }
    }

    /**
     * Get RMI client for specified instance.
     */
    public static RMIClient getRMIClient(String instanceName) {
        //KE FIXME: All of this code is obsolete whent the stub file 
        //is removed.
        RMIClient client = (RMIClient)rmiClientMap.get(instanceName);
        if (client == null) {
            client = new RMIClient(getStubFileName(),
                    getSeedFileName());
            rmiClientMap.put(instanceName, client);
        }
        return client;
    }

    /**
     * Set channel to ready state. This means that the server instance that
     * initialized the channel is ready to serve client requests.
     * @throws RuntimeException if the channel has not been initialized
     */
    public static void setRMIChannelReady() {
        assertAdminServerChannelNotNull();
        server.setChannelReady();
    }

    public static void setRMIChannelStopping() {
        assertAdminServerChannelNotNull();
        server.setChannelStopping();
    }

    /**
     * Set the channel to failed state. If the client detects this state, then
     * it will try to get the port number that caused failure from the channel.
     * @param port port number.
     */
    public static void setRMIChannelAborting(int port) {
        assertAdminServerChannelNotNull();
        server.setChannelAborting(port);
    }
    
    static final String stubFileName = "admch";

    //Begin EE: 4921345 instanceRoot cannot be statically initialized since it relies on
    //a system property which may not be set until startup time. This removes the
    //dependency ond AdminService.
    static String getInstanceRoot() {
        if (instanceRoot == null) {            
            instanceRoot = System.getProperty(SystemPropertyConstants.INSTANCE_ROOT_PROPERTY);
        }
        return instanceRoot;
    }
    //End EE: 4921345 instanceRoot cannot be statically initialized since it relies on
    //a system property which may not be set until startup time. This removes the
    //dependency ond AdminService.
    
    static String getStubFileName() {
        return getInstanceRoot() + fileSeparator 
                + Constants.CONFIG_DIR_NAME + fileSeparator
                + stubFileName;
    }

    static final String seedFileName = "admsn";

    static String getSeedFileName() {
        return getInstanceRoot() + fileSeparator 
                + Constants.CONFIG_DIR_NAME + fileSeparator
                + seedFileName;
    }

    /**
     * Create server object that serves RMI client. If local loopback address
     * can be determined the server object listens on only local loopback
     * address, otherwise it listens on all interfaces (default RMI behavior).
     */
    private static AdminChannelServer createServerObject()
            throws RemoteException {
        AdminChannelServer server = null;
        InetAddress localAddress = getLocalLoopbackAddress();
        if (localAddress == null) {
            server = new AdminChannelServer();
        } else {
            LocalRMIClientSocketFactory csf =
                    new LocalRMIClientSocketFactory(localAddress);
            LocalRMIServerSocketFactory ssf =
                    new LocalRMIServerSocketFactory(localAddress);
            server = new AdminChannelServer(0, csf, ssf);
            server.setLocalAddress(localAddress);
        }
        return server;
    }

    /**
     * Get local loopback address.
     * @return local loopback address, if it can be determined, null otherwise
     */
    private static InetAddress getLocalLoopbackAddress() {
        InetAddress localAddr = null;
        try {
            localAddr = InetAddress.getByName(null);
            if (!localAddr.isLoopbackAddress()) {
                localAddr = null;
            }
        } catch (Throwable t) {
            // Catch all exceptions and return null to the caller
            localAddr = null;
        }
        return localAddr;
    }

    /**
     * Get previous seed. This seed is read either from previous session's
     * shared secret file or initialized using SecureRandom.getSeed (if the
     * shared secret file does not exist). This seed is then used with
     * SecureRandom to generate next key value.
     */
    private static byte[] getPreviousSeed(File seedFile) {
        boolean haveSeed = false;
        byte[] prevSeed = new byte[SEED_LENGTH];

        // Using secure.seed bits to mix in a few extra bits of randomness
        // since we cannot use SecureRandoms built-in seeding.
        // Read bugs 4703002 and 4709460 for some background on this.
        SecureRandom sr = 
            com.sun.enterprise.server.J2EEServer.secureRandom;
        assert (sr != null);    // was initialized early on startup
        sr.setSeed(System.currentTimeMillis());
        
        if (seedFile.exists() && seedFile.canRead()) {
            FileInputStream fis = null;
            try {
                fis = new FileInputStream(seedFile);
                fis.read(prevSeed);
                sr.setSeed(prevSeed);
                sr.nextBytes(prevSeed);
                haveSeed = true;
            } catch (IOException ioe) {
                warn(KEY_READ_ERROR);
                debug(ioe);
            } finally {
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException ioe) {
                    }
                }
            }
        }
        if (!haveSeed) {
            sr.nextBytes(prevSeed);
        }
        return prevSeed;
    }

    /**
     * Save shared secret in file (so that it becomes shared)
     */
    private static void saveSeedToFile(byte[] seed, File seedFile) {
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(seedFile);
            fos.write(seed);
        } catch (IOException ioe) {
            warn(KEY_WRITE_ERROR);
            debug(ioe);
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException ioe) {
                }
            }
        }
    }

    /**
     * Save remote stub for admin channel server to file.
     */
    private static void saveStubToFile(RemoteStub stub) {        
        String fileName = getStubFileName();
        try {
            File file = new File(fileName);
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(stub);
            fos.close();
        } catch (Exception e) {
			String msg = localStrings.getString( "admin.server.core.channel.unable_saving_stub_to_file", fileName );	
            throw new RuntimeException( msg, e );
        }
    }

    /**
     * Cleanup stub file (is invoked on shutdown)
     */
    private static void deleteStubFile() {
        String fileName = getStubFileName();
        new File(fileName).delete();
    }

    /**
     * Assert than Admin server channel is not null.
     * @throws RuntimeException if admin server channel is null.
     */
    private static final void assertAdminServerChannelNotNull() {
        if (server == null) {
			String msg = localStrings.getString( "admin.server.core.channel.admin_server_channel_not_initialized" ); 
            throw new RuntimeException( msg );
        }
    }

    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);
    }

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

    static final String LOCAL_ONLY_ACCESS = "high";
    static final String ALLOW_ALL_ACCESS = "none";

    /**
     * Get access level. FIX to use config parameter
     */
    static String getAccessLevel() {
        return LOCAL_ONLY_ACCESS;
    }

    static final String ENFORCE = "high";
    static final String REQUIRE_KEY = "medium";
    static final String NO_ENFORCE = "low";

    /**
     * Get key check level. FIX to use config parameter.
     */
    static String getKeyCheckLevel() {
        return ENFORCE;
    }

    /**
     * Is Auto Refresh enabled for RMIClient objects (so they will keep on
     * scanning file system for changes to stub file and reset themselves).
     * FIX to use config parameter
     */
    static boolean getClientAutoRefreshEnabled() {
        return true;
    }

    /**
     * How frequently should the RMI client objects refresh themselves (in
     * milliseconds). FIX to use config parameter
     */
    static long getClientAutoRefreshInterval() {
        // 1 minute
        return (1 * 60 * 1000);
    }

    static final String RECONFIG_ENABLE_ERROR = "channel.reconfig_enable_error";
    static final String SERVER_CREATION_ERRCODE = "channel.creation_error";
    static final String KEY_READ_ERROR = "channel.key_read_error";
    static final String KEY_WRITE_ERROR = "channel.key_write_error";
}
