/*
* Licensed Materials - Property of IBM Corp.
* IBM UrbanCode Build
* IBM UrbanCode Deploy
* IBM UrbanCode Release
* IBM AnthillPro
* (c) Copyright IBM Corporation 2002, 2016. All Rights Reserved.
*
* U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by
* GSA ADP Schedule Contract with IBM Corp.
*/
package com.urbancode.air.plugin.websphereliberty;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import com.ibm.websphere.jmx.connector.rest.ConnectorSettings;
import javax.management.remote.JMXServiceURL;
import javax.management.ObjectName;
import javax.management.JMX;
import javax.management.MBeanServerConnection;

public class WebSphereLibertyJMXHelper {
    JMXConnector conn = null;
    JMXServiceURL url = null;
    Map<String, Object> env;
    MBeanServerConnection mbs = null;
    String trustStorePath = null;
    String trustStorePassword = null;
    String routeSetToHost = null;

    public WebSphereLibertyJMXHelper(def userName, 
                                     def userPassword,
                                     def server,
                                     def httpsPort,
                                     def trustStorePath,
                                     def trustStorePassword) 
    {
        this.trustStorePath = trustStorePath;
        this.trustStorePassword = trustStorePassword;
        File trustStoreFile = new File(trustStorePath);
        if (!trustStoreFile.isFile()) {
            System.out.println("Trust store file not found at path : " + trustStoreFile.absolutePath);
            System.exit(1);
        }


        try {
            httpsPort = Integer.valueOf(httpsPort);
        }
        catch (NumberFormatException e) {
            System.out.println("HTTPS Port must be an integer!");
        }
        System.setProperty("javax.net.ssl.trustStore", trustStoreFile.absolutePath);
        System.setProperty("javax.net.ssl.trustStorePassword", trustStorePassword);

        env = new HashMap<String, Object>();

        env.put("jmx.remote.protocol.provider.pkgs", "com.ibm.ws.jmx.connector.client");
        env.put(JMXConnector.CREDENTIALS,  [userName, userPassword] as String[]);
        env.put(ConnectorSettings.DISABLE_HOSTNAME_VERIFICATION, true);
        env.put(ConnectorSettings.READ_TIMEOUT, 2 * 60 * 1000);

        url = new JMXServiceURL("REST", server, httpsPort, "/IBMJMXConnectorREST");
    }

    public void createConn() {
        if (conn == null) {
           conn = JMXConnectorFactory.connect(url, env);
           mbs = conn.getMBeanServerConnection();
        }
    }

    public ObjectName createPluginConfigObjectName() {
        ObjectName fileMonitorObjName = new ObjectName(
            "WebSphere:name=com.ibm.ws.jmx.mbeans.generatePluginConfig");
        return fileMonitorObjName;
    }

    public ObjectName createFileMonitorObjectName() {
        ObjectName fileMonitorObjName = new ObjectName(
            "WebSphere:service=com.ibm.ws.kernel.filemonitor.FileNotificationMBean");
        return fileMonitorObjName;
    }

    public ObjectName createAppObjectName(String appName) {
        ObjectName myAppMBean = new ObjectName(
            "WebSphere:service=com.ibm.websphere.application.ApplicationMBean,name="+appName);
        return myAppMBean;
    }

    public ObjectName createFileTransferObjectName() {
        ObjectName fileTransferObjName = new ObjectName(
            "WebSphere:feature=restConnector,type=FileTransfer,name=FileTransfer");
        return fileTransferObjName;
    }

    public ObjectName createRoutingContextObjectName() {
        ObjectName routingContextObjName = new ObjectName(
            "WebSphere:feature=collectiveController,type=RoutingContext,name=RoutingContext");
        return routingContextObjName;
    }

    public ObjectName createCollectiveRegistrationObjectName() {
        ObjectName collectiveRegistrationObjName = new ObjectName(
            "WebSphere:feature=collectiveController,type=CollectiveRegistration,name=CollectiveRegistration");
        return collectiveRegistrationObjName;
    }

    public ObjectName createServerCommandsObjectName() {
        ObjectName serverCommandsObjName = new ObjectName(
            "WebSphere:feature=collectiveController,type=ServerCommands,name=ServerCommands");
        return serverCommandsObjName;
    }

    public boolean isMBeanRegistered(ObjectName mbean) {
        createConn();
        return mbs.isRegistered(mbean);
    }

    private isAppInState(ObjectName appMBean, String state) {
        createConn();
        boolean correctState = false;
        if (isMBeanRegistered(appMBean)) {
            String curState = (String) mbs.getAttribute(appMBean, "State");  
            if (curState == state) {
                correctState = true;
            }
            else {
                println ("Current state does not match expected!");
                println (curState + " : " + state);
            }
        }
        else {
                println ("Application not currently registered.");
        }
        return correctState;
    }

    public void waitForApplication(String appName, String state, Long waitTimeout) {
        createConn();
        ObjectName myAppMBean =createAppObjectName(appName); 
        def start = System.currentTimeMillis();
        def end = start + (waitTimeout * 1000)
        def started = false;
        while (!started && System.currentTimeMillis() < end) {
            if (isAppInState(myAppMBean, state)) {
                started=true;
            }
            else {
                println ("Sleeping for 3 seconds...");
                Thread.sleep(3*1000);
            }
        }
        
        if (started) {
            println "Application in the started state!";
        }
        else {
            println "Application never started!";
            System.exit(1);
        }
    }

    public void reloadServerXmlConfiguration() {
        ObjectName fileMonitorMBean = createFileMonitorObjectName();
        List createdFiles = new ArrayList<String>();
        List modifiedFiles = new ArrayList<String>();
        List deletedFiles = new ArrayList<String>();

        modifiedFiles.add("server.xml");

        createConn();
        System.out.println("Telling liberty to please reload configuration for server.xml...");
        println mbs.invoke(fileMonitorMBean, "notifyFileChanges", 
                     [createdFiles, modifiedFiles, deletedFiles] as Object[],
                     ["java.util.Collection", "java.util.Collection", "java.util.Collection"] as String[]);
    }

    public void generatePluginConfig(String serverName, String installRoot) {
        String method = "generatePluginConfig";
        String methodDefault = "generateDefaultPluginConfig";
        ObjectName pluginMBean = createPluginConfigObjectName();
        createConn();
        if (serverName && installRoot) {
            System.out.println("Generating plugin config for ${installRoot} and ${serverName}...");
            mbs.invoke(pluginMBean, method, [installRoot, serverName] as Object [],
                       [String.class.name, String.class.name] as String[]);
        }
        else if (!serverName && !installRoot) {
            System.out.println("Generating default plugin config...");
            mbs.invoke(pluginMBean, methodDefault, [] as Object [],
                       [] as String[]);
        }
        else {
            System.out.println("If Install Root or Server Name is configured, the other must be configured as well!");
            System.exit(1);
        }
    }

    private setAppState(String appName, String state, String method) {
        createConn();
        boolean failure = true;
        ObjectName appMBean = createAppObjectName(appName);
        if (!isMBeanRegistered(appMBean)) {
            System.out.println("No application MBean registered for ${appName}. Check that the application is installed in the server.");
        }
        else if (isAppInState(appMBean, state)) {
            System.out.println("Application ${appName} is already in state ${state}!");
            failure = false;
        }
        else {
            System.out.println("Moving application ${appName} to state ${state}...");
            mbs.invoke(appMBean, method, [] as Object[], [] as String[]);
            failure= false;
        }
        if (failure) {
            System.exit(1);
        }
    }
  
    public void startApp(String appName, int readTimeout) {
        if (readTimeout > 0) {
            /* Override default timeout value used in createConn() */
            env.put(ConnectorSettings.READ_TIMEOUT, readTimeout);
        }
        setAppState(appName, "STARTED", "start");
    }

    public void stopApp(String appName) {
        setAppState(appName, "STOPPED", "stop");
    }

    public void uploadFileToCollectiveHost(String host, String srcPath, String destPath, Boolean expand) {
        createConn();
        setRoutingContext(host);

        ObjectName fileTransferMBean = createFileTransferObjectName();
        mbs.invoke(fileTransferMBean, "uploadFile",
                     [srcPath, destPath, expand] as Object[],
                     ["java.lang.String", "java.lang.String", "boolean"] as String[]);
        System.out.println "Uploaded ${destPath}";
    }

    public void setRoutingContext(String host) {
        // Set mbean connection with the routing information for the specified
        // host.  From this point forward, commands using mbs will be routed
        // to the specified collective host.
        if (host != routeSetToHost) {
            ObjectName routingCtxObjectName = createRoutingContextObjectName()
            boolean routingCtxObj = mbs.invoke(routingCtxObjectName,"assignHostContext",[host] as Object[],["java.lang.String"] as String[])
            if (routingCtxObj != true) {
                throw new RuntimeException("Error creating routing context to target host.  The return value from invoke was: " + String(routingCtxObj))
            }
            routeSetToHost = host;
            System.out.println("Routing context to host ${host} has been set.");
        }
    }

    public int startServerViaController(String wlpDir, String serverName, String serverHost) {
        createConn();
        setRoutingContext(serverHost);
        def wlpUsrDir = wlpDir + File.separator + "usr";
        ObjectName serverCommandsMBean = createServerCommandsObjectName();
        def startResult = mbs.invoke(serverCommandsMBean, "startServer",
                     [serverHost, wlpUsrDir, serverName, ""] as Object[],
                     ["java.lang.String", "java.lang.String", "java.lang.String", "java.lang.String"] as String[]);
        def rt = startResult.get("returnCode");
        /* returnCode of 0 (server started) or 1 (server already started) is good */
        if (rt == 0) {
            System.out.println("Server " + serverName + " started successfully.");
        }
        else if (rt == 1) {
            System.out.println("Server " + serverName + " already started.");
        }
        else {
            System.out.println("Server " + serverName + " failed to start.  Return code: " + rt.toString());
        }
        return rt;
    }

    public int stopServerViaController(String wlpDir, String serverName, String serverHost) {
        createConn();
        setRoutingContext(serverHost);
        def wlpUsrDir = wlpDir + File.separator + "usr";
        ObjectName serverCommandsMBean = createServerCommandsObjectName();
        def stopResult = mbs.invoke(serverCommandsMBean, "stopServer",
                     [serverHost, wlpUsrDir, serverName, ""] as Object[],
                     ["java.lang.String", "java.lang.String", "java.lang.String", "java.lang.String"] as String[]);
        def rt = stopResult.get("returnCode");
        /* returnCode of 0 (server stopped) or 1 (server already stopped) is good */
        if (rt == 0) {
            System.out.println("Server " + serverName + " stopped successfully.");
        }
        else if (rt == 1) {
            System.out.println("Server " + serverName + " already stopped.");
        }
        else {
            System.out.println("Server " + serverName + " failed to stop.  Return code: " + rt.toString());
        }
        return rt;
    }

    public void joinCollectiveViaController(String wlpDir, String serverName, String serverHost, String keystorePassword, hostAuthInfo) {
        createConn();
        setRoutingContext(serverHost);
        def wlpUsrDir = wlpDir + File.separator + "usr";
        def certProps = ["collectiveTrustKeystorePassword" : trustStorePassword]

        ObjectName collectiveRegistrationMBean = createCollectiveRegistrationObjectName();
        def joinResult = mbs.invoke(collectiveRegistrationMBean, "join",
                     [serverHost, wlpUsrDir, serverName, wlpDir, keystorePassword, certProps, hostAuthInfo] as Object[],
                     ["java.lang.String", "java.lang.String", "java.lang.String", "java.lang.String", "java.lang.String", "java.util.Map", "java.util.Map"] as String[]);

        // Upload the keystore file data returned in joinResult to the newly
        // joined member server
        uploadBytes("serverIdentity", joinResult.get("serverIdentity.jks"),
                    wlpUsrDir + File.separator + "servers" + File.separator + serverName + File.separator + "resources" + File.separator + "collective" + File.separator + "serverIdentity.jks");

        uploadBytes("collectiveTrust", joinResult.get("collectiveTrust.jks"),
                    wlpUsrDir + File.separator + "servers" + File.separator + serverName + File.separator + "resources" + File.separator + "collective" + File.separator + "collectiveTrust.jks");

        uploadBytes("key", joinResult.get("key.jks"),
                    wlpUsrDir + File.separator + "servers" + File.separator + serverName + File.separator + "resources" + File.separator + "security" + File.separator + "key.jks");

        uploadBytes("trust", joinResult.get("trust.jks"),
                    wlpUsrDir + File.separator + "servers" + File.separator + serverName + File.separator + "resources" + File.separator + "security" + File.separator + "trust.jks");

        // Finally, need to work around issue where the newly joined server
        // won't connect to the collective controller if the file
        // resources/collective/.collective does not exist.  This file gets
        // created when the join command is run on the local machine, but not
        // when run via JMX.  Supposed to be fixed in a later version of Liberty
        uploadBytes("marker", "Collective Marker File".getBytes(),
                    wlpUsrDir + File.separator + "servers" + File.separator + serverName + File.separator + "resources" + File.separator + "collective" + File.separator + ".collective");

    }

    // Utility method to upload items to a controller member.  Assumes the
    // routing context has already been setup.
    protected void uploadBytes(String tmpPrefix, byte[] srcBytes, String destPath) {
        ObjectName fileTransferMBean = createFileTransferObjectName();
        def tmpFilePath = createTempFile(tmpPrefix, srcBytes);
        mbs.invoke(fileTransferMBean, "uploadFile",
            [tmpFilePath, destPath, false] as Object[],
            ["java.lang.String", "java.lang.String", "boolean"] as String[]);
        System.out.println "Uploaded ${destPath}";
    }

    protected String createTempFile(String prefix, byte[] bytes) {
        def tmpFile = File.createTempFile(prefix, ".tmp");
        tmpFile.deleteOnExit();
        tmpFile.setBytes(bytes);
        return tmpFile.absolutePath;
    }
}
