/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, 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/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.enterprise.jbi.serviceengine.core;

import com.sun.enterprise.admin.server.core.AdminService;
import com.sun.enterprise.config.serverbeans.ServerHelper;
import com.sun.enterprise.deployment.DeploymentContext;
import com.sun.enterprise.deployment.Application;
import com.sun.enterprise.deployment.archivist.Archivist;
import com.sun.enterprise.deployment.archivist.ArchivistFactory;
import com.sun.enterprise.deployment.archivist.ApplicationArchivist;
import com.sun.enterprise.deployment.backend.DeploymentStatus;
import com.sun.enterprise.deployment.backend.J2EEModuleExploder;
import com.sun.enterprise.deployment.client.DeploymentFacility;
import com.sun.enterprise.deployment.client.DeploymentFacilityFactory;
import com.sun.enterprise.deployment.client.JESProgressObject;
import com.sun.enterprise.deployment.deploy.shared.AbstractArchive;
import com.sun.enterprise.deployment.deploy.shared.ArchiveFactory;
import com.sun.enterprise.deployment.deploy.shared.FileArchive;
import com.sun.enterprise.deployment.deploy.shared.OutputJarArchive;
import com.sun.enterprise.deployment.util.DeploymentProperties;
import com.sun.enterprise.deployment.util.ModuleDescriptor;
import com.sun.enterprise.jbi.serviceengine.util.JBIConstants;
import com.sun.enterprise.jbi.serviceengine.util.Util;
import com.sun.enterprise.jbi.serviceengine.util.soap.WSDLConverter;
import com.sun.enterprise.server.ApplicationServer;
import com.sun.enterprise.util.shared.ArchivistUtils;
import com.sun.enterprise.util.FileUtil;
import com.sun.enterprise.util.io.FileUtils;
import com.sun.logging.LogDomains;

import javax.enterprise.deploy.shared.ModuleType;
import javax.enterprise.deploy.spi.Target;
import javax.jbi.component.ComponentContext;
import javax.jbi.component.ServiceUnitManager;
import javax.jbi.management.DeploymentException;
import java.io.*;
import java.util.*;
import java.util.zip.ZipFile;
import java.util.zip.ZipEntry;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.ObjectName;

/**
 * Represents JavaEE Service Engine ServiceUnitManager, responsible for
 * deploy/undeploy/start/stop of JavaEE application packaged inside the service
 * assembly.
 *
 * @author bhavanishankar@dev.java.net
 */
public class JavaEEServiceEngineSUManager implements ServiceUnitManager, JBIConstants {
    
    protected static final Logger logger=LogDomains.getLogger(LogDomains.DPL_LOGGER);
    
    private String className = "JavaEEServiceEngineSUManager :: ";
    private DeploymentFacility deployer;
    private String target = "server"; // default target is DAS instance.
    private JBIEndpointManager epManager;
    private Set<String> dummyCompApps;

    /**
     * Creates a new instance of JavaEEServiceEngineServiceUnitManager
     */
    public JavaEEServiceEngineSUManager() {
        createDeployer();
        epManager = new JBIEndpointManager();
        dummyCompApps = new HashSet<String>();
    }
    
    // START :: ServiceUnitManager callbacks.
    
    public void init(String suId, String suPath) throws DeploymentException {
        String methodSig = className + "init(String, String)";
        logger.log(Level.FINE,
                methodSig + " suId = " + suId + ". suPath = " + suPath);
        try {
            if(isDummyApp(suPath))
                dummyCompApps.add(suId);

            epManager.storeAllEndpoints(suPath, suId);
        } catch (Exception e) {
            throw new DeploymentException(e);
        }
        
        /** 
         * Start the Java EE app & enable the endpoints in NMR
         */
        if (isDAS() && !isPrivateMBeanRegistered()) {
            logger.log(Level.FINE,
                    methodSig +
                    " either a non-DAS instance or start is done through" +
                    " private MBean, hence skipping start.");
            doStart(suId, target);
        }
        
        try {
            epManager.startAllEndpoints(suId);
        } catch (Exception e) {
            throw new DeploymentException(e);
        }
        
    }
    
    public void shutDown(String suId) throws DeploymentException {
        String methodSig = className + "shutDown(String)";
        logger.log(Level.FINE, methodSig + " suId = " + suId);
        try {
            dummyCompApps.remove(suId);
            epManager.removeAllEndpoints(suId);
        } catch (Exception e) {
            throw new DeploymentException(e);
        }
    }
    
    /**
     * Only deploy the JavaEE application and keep it in 'disable'
     * state until JBI fires start event.
     */
    public String deploy(String suId, String suPath) throws
            DeploymentException {
        String methodSig = className + "deploy(String, String)";
        logger.log(Level.FINE,
                methodSig + " suId = " + suId + ". suPath = " + suPath);
        
        if (!isDAS() || isPrivateMBeanRegistered()) {
            logger.log(Level.FINE,
                    methodSig + " either a non-DAS instance or deployment is done " +
                    "through private MBean, hence skipping deploy.");
            return Util.buildManagementMessage(
                    "STATUS_MSG",
                    "deploy",
                    "SUCCESS",
                    null,
                    suId,
                    null,
                    null);
        }
        return doDeploy(suId, suPath, target);
    }
    
    public String undeploy(String suId, String suPath) throws
            DeploymentException {
        String methodSig = className + "undeploy(String, String)";
        logger.log(Level.FINE,
                methodSig + " suId = " + suId + ". suPath = " + suPath);
        
        if (!isDAS() || isPrivateMBeanRegistered()) {
            logger.log(Level.FINE,
                    methodSig +
                    " either a non-DAS instance or undeployment is done" +
                    " through private MBean, hence skipping undeploy.");
            return Util.buildManagementMessage(
                    "STATUS_MSG",
                    "undeploy",
                    "SUCCESS",
                    null,
                    suId,
                    null,
                    null);
        }
        return doUnDeploy(suId, target);
    }
    
    /**
     * Enable the deployed JavaEE application during the start event.
     */
    public void start(String suId) throws DeploymentException {
        String methodSig = className + "start(String) :: NO-OP";
        logger.log(Level.FINE, methodSig + " suId = " + suId);
    }
    
    /**
     * Disable the deployed JavaEE application during the stop event.
     */
    public void stop(String suId) throws DeploymentException {
        String methodSig = className + "stop(String)";
        logger.log(Level.FINE, methodSig + " suId = " + suId);

        if (isDAS() && !isPrivateMBeanRegistered()) {
            logger.log(Level.FINE,
                    methodSig +
                    " either a non-DAS instance or deployment is done " +
                    "through private MBean, hence skipping stop.");
            doStop(suId, target);
        }
        
        try {
            epManager.stopAllEndpoints(suId);
        } catch (Exception e) {
            throw new DeploymentException(e);
        }
    }
    
    // END :: ServiceUnitManager callbacks.
    
    // START :: Methods exposed to the private JavaEEDeployerMBean.
    
    protected String doDeploy(String serviceUnitName,String appLocation,String target) {
        String methodSig = className + "doDeploy(String,String,String,String) "; // used for logging.
        boolean deploymentSuccessful = true;
        String deploymentError = null;
        Throwable deploymentException = null;
        String archivePath = null;
        AbstractArchive arch = null;
        try {
            if (!isDummyApp(appLocation)) {
                if(!(USED_WITH_NON_SOAP_WSDL.equalsIgnoreCase(System.getProperty(USED_WITH)))) {
                    appLocation = processWSDLs(appLocation);
                }
                archivePath = (new File(appLocation)).isDirectory() 
                    ? createArchive(appLocation, serviceUnitName)
                    : appLocation;
                
                logger.log(Level.FINE,
                        methodSig + " appName = " + serviceUnitName + ", archivePath = " +
                        archivePath + ", target = " + target);

                Map deployOptions = getDefaultDeploymentOptions(archivePath,
                        serviceUnitName,
                        target);
                arch = (new ArchiveFactory()).openArchive(archivePath);
                AbstractArchive plan = null;
                Target[] targets = deployer.createTargets(new String[]{target});
                logger.log(Level.FINE, methodSig + " calling backend deploy");
                JESProgressObject progressObject =
                        deployer.deploy(targets, arch, plan, deployOptions);
                DeploymentStatus status = deployer.waitFor(progressObject);
                logger.log(Level.FINE,
                        methodSig + " deployment complete. status = " + status.getStatus());
                if (status.getStatus() != DeploymentStatus.SUCCESS) {
                    deploymentSuccessful = false;
                    deploymentError = methodSig + status.toString();
                    deploymentException = status.getStageException();
                }
            } else {
                dummyCompApps.add(serviceUnitName);
            }
        } catch (Exception ex) {
            deploymentSuccessful = false;
            deploymentError = methodSig + ex.getMessage();
            deploymentException = ex;
        } finally {
            if(arch != null) {
                try {
                    arch.close();
                } catch(Exception ex) {
                    logger.log(Level.SEVERE, ex.getMessage(), ex);
                }
            }
        }
        
        if(archivePath != null && (new File(appLocation)).isDirectory() ) {
            try {
                boolean deleteSuccessful = new File(archivePath).delete();
                if(!deleteSuccessful) {
                    logger.log(Level.SEVERE, "Unable to delete the archive " + archivePath);
                }
            } catch(Exception ex) {
                logger.log(Level.SEVERE, ex.getMessage(), ex);
            }
        }
        
        if(deploymentSuccessful) {
            logger.log(Level.FINE, methodSig + " successfully deployed " + serviceUnitName);
            return Util.buildManagementMessage(
                    "STATUS_MSG",
                    "deploy",
                    "SUCCESS",
                    null,
                    serviceUnitName,
                    null,
                    null);
        } else {
            logger.log(Level.SEVERE, deploymentError, deploymentException);
            return Util.buildManagementMessage(
                    "EXCEPTION_MSG",
                    "deploy",
                    "FAILED",
                    null,
                    serviceUnitName,
                    deploymentError,
                    deploymentException);
        }
    }
    
    protected String doUnDeploy(String serviceUnitName,String target) {
        String methodSig = className + "doUnDeploy(String,String,String) "; // used for logging.
        boolean undeploymentSuccessful = true;
        String undeploymentError = null;
        Throwable undeploymentException = null;
        try {
            //TODO: clear registries (wsdlCache)
            if(!dummyCompApps.remove(serviceUnitName)) {
                Target[] targets = deployer.createTargets(new String[]{target});
                JESProgressObject progressObject =
                        deployer.undeploy(targets, serviceUnitName, getUnDeploymentOptions());
                DeploymentStatus status = deployer.waitFor(progressObject);
                if (status.getStatus() != DeploymentStatus.SUCCESS) {
                    undeploymentSuccessful = false;
                    undeploymentError = methodSig + status.toString();
                    undeploymentException = status.getStageException();
                }
            }
            
        } catch (Exception ex) {
            undeploymentSuccessful = false;
            undeploymentError = methodSig + ex.getMessage();
            undeploymentException = ex;
        }
        
        if(undeploymentSuccessful) {
            logger.log(Level.FINE, methodSig + " successfully undeployed " + serviceUnitName);
            return Util.buildManagementMessage(
                    "STATUS_MSG",
                    "undeploy",
                    "SUCCESS",
                    null,
                    serviceUnitName,
                    null,
                    null);
        } else {
            logger.log(Level.SEVERE, undeploymentError, undeploymentException);
            return Util.buildManagementMessage(
                    "EXCEPTION_MSG",
                    "undeploy",
                    "FAILED",
                    null,
                    serviceUnitName,
                    undeploymentError,
                    undeploymentException);
        }
    }
    
    protected void doStart(String serviceUnitName, String target) {
        String methodSig = className + "doStart(String,String) "; // used for logging.
        boolean startSuccessful = true;
        String startError = null;
        Throwable startException = null;
        
        try {
            if(!dummyCompApps.contains(serviceUnitName)) {
                Target[] targets = deployer.createTargets(new String[]{target});
                JESProgressObject progressObject =
                        deployer.enable(targets, serviceUnitName);
                DeploymentStatus status = deployer.waitFor(progressObject);
                if (status.getStatus() != DeploymentStatus.SUCCESS) {
                    startSuccessful = false;
                    startError = methodSig + status.toString();
                    startException = status.getStageException();
                }
            }
            
        } catch (Exception ex) {
            startSuccessful = false;
            startError = methodSig + ex.getMessage();
            startException = ex;
        }
        
        if(startSuccessful) {
            logger.log(Level.FINE, methodSig + " successfully started " + serviceUnitName);
        } else {
            logger.log(Level.SEVERE, startError, startException);
        }
    }
    
    protected void doStop(String serviceUnitName, String target) {
        String methodSig = className + "doStop(String,String) "; // used for logging.
        boolean stopSuccessful = true;
        String stopError = null;
        Throwable stopException = null;
        
        try {
            if(!dummyCompApps.contains(serviceUnitName)) {
                Target[] targets = deployer.createTargets(new String[]{target});
                JESProgressObject progressObject =
                        deployer.disable(targets, serviceUnitName);
                DeploymentStatus status = deployer.waitFor(progressObject);
                if (status.getStatus() != DeploymentStatus.SUCCESS) {
                    stopSuccessful = false;
                    stopError = methodSig + status.toString();
                    stopException = status.getStageException();
                }
            }
            
        } catch (Exception ex) {
            stopSuccessful = false;
            stopError = methodSig + ex.getMessage();
            stopException = ex;
        }
        
        if(stopSuccessful) {
            logger.log(Level.FINE, methodSig + " successfully stopped " + serviceUnitName);
        } else {
            logger.log(Level.SEVERE, stopError, stopException);
        }
    }
    
    // END :: Methods exposed to the private JavaEEDeployerMBean.
    
    // START : Utility methods.
    
    private String createArchive(String dir, String archiveName) {
        String archivePath;
        try {
            Archivist archivist = ArchivistFactory.getArchivistForArchive(dir);
            ModuleType moduleType = archivist.getModuleType();
            String pathExtn = ".jar";
            if (ModuleType.EAR.equals(moduleType)) {
                pathExtn = ".ear";
            } else if (ModuleType.WAR.equals(moduleType)) {
                pathExtn = ".war";
            } else if (ModuleType.RAR.equals(moduleType)) {
                pathExtn = ".rar";
            }
            String workspaceDir =
                    JavaEEServiceEngineContext.getInstance().
                    getJBIContext().getWorkspaceRoot();
            archivePath = workspaceDir + File.separator + archiveName + pathExtn;
            if (ModuleType.EAR.equals(moduleType)) {
                createEar(dir, archivePath);
            } else {
                createJar(dir, archivePath);
            }
        } catch (Exception e) {
            logger.log(Level.SEVERE, e.getMessage(), e);
            archivePath = null;
        }
        return archivePath;
    }
    
    private void createJar(String sourcePath, String destinationPath)
    throws IOException {
        FileArchive source = new FileArchive();
        OutputJarArchive destination = new OutputJarArchive();
        try {
            source.open(sourcePath);
            Enumeration entries = source.entries();
            destination.create(destinationPath);
            while(entries.hasMoreElements()) {
                String entry = String.class.cast(entries.nextElement());
                InputStream is = null;
                OutputStream os = null;
                try {
                    is = source.getEntry(entry);
                    os = destination.putNextEntry(entry);
                    ArchivistUtils.copyWithoutClose(is, os);
                } finally {
                    if (is != null) is.close();
                    if (os != null) destination.closeEntry();
                }
            }
        } finally {
            source.close();
            destination.close();
        }
    }

    private void createEar(String explodedDir, String earFilePath)
            throws Exception {
        // create the application object. 
        ApplicationArchivist archivist = new ApplicationArchivist();
        FileArchive appArchive = new FileArchive();
        appArchive.open(new File(explodedDir).getAbsolutePath());
        archivist.setManifest(appArchive.getManifest());
        Application application = null;
        if (archivist.hasStandardDeploymentDescriptor(appArchive)) {
            application = (Application)
                    archivist.readStandardDeploymentDescriptor(appArchive);
        } else {
            application = Application.createApplication(appArchive, true, true);
        }
        archivist.setDescriptor(application);

        Iterator<ModuleDescriptor> bundles = application.getModules();
        // archive all the modules first.
        while (bundles.hasNext()) {
            ModuleDescriptor bundle = bundles.next();
            String moduleName = bundle.getArchiveUri(); // eg., EJBImplementWSDL.jar
            String massagedModuleName = FileUtils.makeFriendlyFilename(moduleName); // eg., EJBImplementWSDL_jar
            String explodedModulePath = explodedDir + File.separator + massagedModuleName;
            String moduleArhivePath = explodedDir + File.separator + moduleName;
            createJar(explodedModulePath, moduleArhivePath);
            DeploymentContext.deleteDirectory(new File(explodedModulePath)); // delete EJBImplementWSDL_jar
        }
        // now create the .ear with archived modules.
        createJar(explodedDir, earFilePath);
    }
    
    private void explodeArchive(String src, String dest) throws IOException {
        File srcFile = new File(src);
        File destFile = new File(dest);
        if(destFile.exists()) {
            DeploymentContext.deleteDirectory(destFile);
            destFile.mkdir();
        }
        Archivist archivist = ArchivistFactory.getArchivistForArchive(srcFile);
        try {
            ModuleType moduleType = archivist.getModuleType();
            if(ModuleType.EAR.equals(moduleType)) {
                J2EEModuleExploder.explodeEar(srcFile, destFile);
            } else if ( ModuleType.EJB.equals(moduleType) ||
                    ModuleType.CAR.equals(moduleType) ||
                    ModuleType.RAR.equals(moduleType) ||
                    ModuleType.WAR.equals(moduleType) ) {
                J2EEModuleExploder.explodeJar(srcFile, destFile);
            }
        } catch (Exception e) {
            IOException ioe = new IOException(e.getMessage());
            ioe.initCause(e);
            throw ioe;
        }
    }
    
    private Map getDefaultDeploymentOptions(String archivePath,
            String appName,
            String target) {
        DeploymentProperties deploymentOptions = new DeploymentProperties();
        deploymentOptions.setName(appName);
        deploymentOptions.setForce(true);
        deploymentOptions.setArchiveName(archivePath);
        deploymentOptions.setEnable(false);
        deploymentOptions.setTarget(target);
        deploymentOptions.setExternallyManaged(true);
        return deploymentOptions;
    }

    private Map getUnDeploymentOptions() {
        DeploymentProperties undeploymentOptions = new DeploymentProperties();
        undeploymentOptions.setExternallyManaged(true);
        return undeploymentOptions;
    }
    
    private boolean isPrivateMBeanRegistered() {
        try {
            ComponentContext jbiContext =
                    JavaEEServiceEngineContext.getInstance().getJBIContext();
            ObjectName privateMBeanName =
                    jbiContext.getMBeanNames().createCustomComponentMBeanName("JavaEEDeployer");
            boolean isPrivateMBeanRegistered =
                    jbiContext.getMBeanServer().isRegistered(privateMBeanName);
            logger.log(Level.FINE,
                    "isPrivateMBeanRegistered = " + isPrivateMBeanRegistered);
            return isPrivateMBeanRegistered;
        } catch(Exception ex) {
            logger.log(Level.SEVERE, ex.getMessage(), ex);
            return false;
        }
    }
    
    private void createDeployer() {
        deployer = DeploymentFacilityFactory.getLocalDeploymentFacility();
    }
    
    private boolean isDAS() {
        try {
            return ServerHelper.isDAS(
                    AdminService.getAdminService().getAdminContext().getAdminConfigContext(),
                    ApplicationServer.getServerContext().getInstanceName());
        } catch (Exception ex) {
            logger.log(Level.SEVERE, className + ex.getMessage(), ex);
            //better be more restrictive by returning false
            return false;
        }
    }

    /**
     * Check whether this is a dummy JavaEE application. In such case deployment 
     * will be skipped as the application will be deployed outside of the 
     * composite application.
     * In a dummy application allowed entries are -
     * META-INF/jbi.xml and META-INF/MANINFEST.MF.
     */
    private boolean isDummyApp(String appLocation) throws IOException {
        File app = new File(appLocation);
        if(app.isDirectory()) {
            for (Object fileObj : FileUtil.getAllFilesAndDirectoriesUnder(app)) {
                String path = ((File)fileObj).getPath();
                if(!("META-INF"+File.separator+"jbi.xml").equalsIgnoreCase(path) &&
                        !("META-INF"+File.separator+"MANIFEST.MF").equalsIgnoreCase(path) &&
                        !"META-INF".equalsIgnoreCase(path))
                    return false;
            }
        } else {
            ZipFile zipFile = new ZipFile(appLocation);
            for (Enumeration e = zipFile.entries(); e.hasMoreElements();) {
                String entryName = ((ZipEntry)e.nextElement()).getName();
                if(!("META-INF/jbi.xml").equalsIgnoreCase(entryName) &&
                        !("META-INF/MANIFEST.MF").equalsIgnoreCase(entryName) &&
                        !("META-INF/").equalsIgnoreCase(entryName)) {
                    return false;
                }
            }
        }
        return true;
    }
    
    /**
     * Convert all the NonSOAP WSDLs under appLocation to SOAP WSDLs.
     */
    private String processWSDLs(String appLocation) {
        
        String newAppLocation;
        
        if(!(new File(appLocation).isDirectory())) {
            try {
                String workspaceDir =
                        JavaEEServiceEngineContext.getInstance().
                        getJBIContext().getWorkspaceRoot();
                
                newAppLocation = workspaceDir + File.separator + "exploded";
                
                explodeArchive(appLocation, newAppLocation);
            } catch(Exception ex) {
                logger.log(Level.SEVERE, ex.getMessage(), ex);
                return appLocation;
            }
        } else { // Ideally the appLocation contents should be copied onto newAppLocation.
            newAppLocation = appLocation;
        }
        
        return
                WSDLConverter.convertWSDLs(newAppLocation).isEmpty()
                ? appLocation
                : newAppLocation;
    }
    
    // END : Utility methods.
}
