/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */

package org.apache.synapse.deployers;

import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMException;
import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.axiom.om.util.StAXUtils;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.deployment.AbstractDeployer;
import org.apache.axis2.deployment.DeploymentException;
import org.apache.axis2.deployment.repository.util.DeploymentFileData;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.util.XMLPrettyPrinter;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.ServerConfigurationInformation;
import org.apache.synapse.ServerContextInformation;
import org.apache.synapse.ServerState;
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.config.SynapseConfiguration;
import org.apache.synapse.core.SynapseEnvironment;

import javax.xml.stream.XMLStreamException;
import java.io.*;
import java.util.Properties;

/**
 * Implements the generic logic for the synapse artifact deployment and provide a deployment
 * framework for the synapse.</p>
 *
 * <p>Any  synapse artifact which requires the hot deployment or hot update features should extend
 * this and just needs to concentrate on the deployment logic. By default setting the file
 * extension and directory dynamically is not supported.
 *
 * @see org.apache.axis2.deployment.Deployer
 */
public abstract class AbstractSynapseArtifactDeployer extends AbstractDeployer {

    private static final Log log = LogFactory.getLog(AbstractSynapseArtifactDeployer.class);
    protected  Log deployerLog;
    protected ConfigurationContext cfgCtx;
    protected String customLogContent;

    protected AbstractSynapseArtifactDeployer() {
        deployerLog = LogFactory.getLog(this.getClass());
    }

    /**
     * Initializes the Synapse artifact deployment
     * 
     * @param configCtx Axis2 ConfigurationContext
     */
    public void init(ConfigurationContext configCtx) {
        this.cfgCtx = configCtx;
    }

    private boolean isHotDeploymentEnabled() {
        try {
            return getSynapseConfiguration().isAllowHotUpdate();
        } catch (DeploymentException e) {
            log.warn("Error while retrieving the SynapseConfiguration", e);
            return false;
        }
    }

    /**
     * This method is called by the axis2 deployment framework and it performs a synapse artifact
     * specific yet common across all the artifacts, set of tasks and delegate the actual deployment
     * to the respective artifact deployers.
     *
     * @param deploymentFileData file to be used for the deployment
     * @throws org.apache.axis2.deployment.DeploymentException in-case of an error in deploying the file
     * 
     * @see org.apache.synapse.deployers.AbstractSynapseArtifactDeployer#deploySynapseArtifact(org.apache.axiom.om.OMElement,
     * String,java.util.Properties)
     */
    public void deploy(DeploymentFileData deploymentFileData) throws DeploymentException {

//        CustomLogSetter.getInstance().setLogAppender(customLogContent);
        if (!isHotDeploymentEnabled()) {
            if (log.isDebugEnabled()) {
                log.debug("Hot deployment has been suspended - Ignoring");
            }
            return;
        }

        String filename = SynapseArtifactDeploymentStore.getNormalizedAbsolutePath(
                deploymentFileData.getAbsolutePath());
        if (log.isDebugEnabled()) {
            log.debug("Deployment of the synapse artifact from file : " + filename + " : STARTED");
        }

        if (getServerContextInformation().getServerState() != ServerState.STARTED) {
            // synapse server has not yet being started
            if (log.isDebugEnabled()) {
                log.debug("Skipped the artifact deployment (since the Synapse " +
                        "server doesn't seem to be started yet), from file : "
                        + deploymentFileData.getAbsolutePath());
            }
            return;
        }

        SynapseArtifactDeploymentStore deploymentStore =
                getSynapseConfiguration().getArtifactDeploymentStore();

        // check whether this is triggered by a restore, if it is a restore we do not want to
        // deploy it again
        if (deploymentStore.isRestoredFile(filename)) {
            if (log.isDebugEnabled()) {
                log.debug("Restored artifact detected with filename : " + filename);
            }
            // only one deployment trigger can happen after a restore and hence remove it from
            // restoredFiles at the first hit, allowing the further deployments/updates to take
            // place as usual
            deploymentStore.removeRestoredFile(filename);
            return;
        }
        
        try {
            InputStream in = FileUtils.openInputStream(new File(filename));
            try {
                // construct the xml element from the file, it has to be XML,
                // since all synapse artifacts are XML based
                OMElement element = new StAXOMBuilder(
                        StAXUtils.createXMLStreamReader(in)).getDocumentElement();
                Properties properties = new Properties();
                properties.put(SynapseConstants.CLASS_MEDIATOR_LOADERS,
                               deploymentStore.getClassMediatorClassLoaders());
                properties.put(SynapseConstants.RESOLVE_ROOT, getSynapseEnvironment()
                        .getServerContextInformation()
                        .getServerConfigurationInformation().getResolveRoot());
                String artifactName = null;
                if (deploymentStore.isUpdatingArtifact(filename)) {

                    if (log.isDebugEnabled()) {
                        log.debug("Updating artifact detected with filename : " + filename);
                    }
                    // this is an hot-update case
                    String existingArtifactName
                            = deploymentStore.getUpdatingArtifactWithFileName(filename);
                    deploymentStore.removeUpdatingArtifact(filename);
                    try {
                        artifactName = updateSynapseArtifact(
                                element, filename, existingArtifactName, properties);
                    } catch (SynapseArtifactDeploymentException | OMException ex) {
                        log.error("Update of the Synapse Artifact from file : " + filename + " : Failed!", ex);
                        log.info("The updated file has been backed up into : " +
                                 backupFile(deploymentFileData.getFile()));
                        log.info("Restoring the existing artifact into the file : " + filename);
                        restoreSynapseArtifact(existingArtifactName);
                        deploymentStore.addArtifact(filename, existingArtifactName);
                        artifactName = existingArtifactName;
                        throw new DeploymentException(ex);
                    }
                } else {
                    // new artifact hot-deployment case
                    try {
                        // When someone deploys either main or fault sequence what actually happens is they simply
                        // update the existing sequences.
                        if (filename.matches(".*/main-\\d+\\.\\d+\\.\\d+\\.xml")) {
                            artifactName = updateDefaultSequence(filename, element, properties,
                                    deploymentStore.getMainSeqLstUpdatedFile(), deploymentStore);

                            String mainSeqFileName = filename.substring(filename.lastIndexOf(File.separator) + 1);
                            deploymentStore.setMainSeqLstUpdatedFile(mainSeqFileName);

                        } else if (filename.matches(".*/fault-\\d+\\.\\d+\\.\\d+\\.xml")) {
                            artifactName = updateDefaultSequence(filename, element, properties,
                                    deploymentStore.getFaultSeqLstUpdatedFile(), deploymentStore);

                            String faultSeqFileName = filename.substring(filename.lastIndexOf(File.separator) + 1);
                            deploymentStore.setFaultSeqLstUpdatedFile(faultSeqFileName);

                        } else {
                            artifactName = deploySynapseArtifact(element, filename, properties);
                        }
                    } catch (SynapseArtifactDeploymentException sade) {
                        log.error("Deployment of the Synapse Artifact from file : "
                                + filename + " : Failed!", sade);
                        log.info("The file has been backed up into : "
                                + backupFile(deploymentFileData.getFile()));
                        throw new DeploymentException(sade);
                    }
                }
                if (artifactName != null) {
                    deploymentStore.addArtifact(filename, artifactName);
                }
            } finally {
                in.close();
            }
        } catch (IOException ex) {
            handleDeploymentError("Deployment of synapse artifact failed. Error reading "
                    + filename + " : " + ex.getMessage(), ex, filename);
            throw new DeploymentException(ex);
        } catch (XMLStreamException ex) {
            handleDeploymentError("Deployment of synapse artifact failed. Error parsing "
                    + filename + " : " + ex.getMessage(), ex, filename);
            throw new DeploymentException(ex);
        } catch (OMException ex) {
            handleDeploymentError("Deployment of synapse artifact failed. Error parsing "
                    + filename + " : " + ex.getMessage(), ex, filename);
            throw new DeploymentException(ex);
        }

        if (log.isDebugEnabled()) {
            log.debug("Deployment of the synapse artifact from file : "
                    + filename + " : COMPLETED");
        }
    }

    /**
     * This is the method called by the axis2 framework for undeployment of the artifacts. As in
     * the deploy case this performs some common tasks across all the synapse artifacts and fall
     * back to the artifact specific logic of undeployment.
     *
     * @param fileName file describing the artifact to be undeployed
     * @throws org.apache.axis2.deployment.DeploymentException in case of an error in undeployment
     *
     * @see AbstractSynapseArtifactDeployer#undeploySynapseArtifact(
     * String)
     */
    public void undeploy(String fileName) throws DeploymentException {

        if (!isHotDeploymentEnabled()) {
            if (log.isDebugEnabled()) {
                log.debug("Hot deployment has been suspended - Ignoring");
            }
            return;
        }

        fileName = SynapseArtifactDeploymentStore.getNormalizedAbsolutePath(fileName);
        if (log.isDebugEnabled()) {
            log.debug("Undeployment of the synapse artifact from file : "
                    + fileName + " : STARTED");
        }

        SynapseArtifactDeploymentStore deploymentStore =
                getSynapseConfiguration().getArtifactDeploymentStore();

        // We want to eliminate the undeployment when we are backing up these files
        if (deploymentStore.isBackedUpArtifact(fileName)) {

            if (log.isDebugEnabled()) {
                log.debug("BackedUp artifact detected with filename : " + fileName);
            }
            // only one undeployment trigger can happen after a backup and hence remove it from
            // backedUpFiles at the first hit, allowing the further undeployment ops to take place
            // as usual
            deploymentStore.removeBackedUpArtifact(fileName);
            return;
        }

        if (deploymentStore.containsFileName(fileName)) {
            File undeployingFile = new File(fileName);
            // axis2 treats Hot-Update as (Undeployment + deployment), where synapse needs to
            // differentiate the Hot-Update from the above two, since it needs some validations for
            // a real undeployment. Also this makes sure a zero downtime of the synapse artifacts
            // which are being Hot-deployed
            if (undeployingFile.exists()) {
                if (log.isDebugEnabled()) {
                    log.debug("Marking artifact as updating from file : " + fileName);
                }
                // if the file exists, which means it has been updated and is a Hot-Update case
                if (!deploymentStore.isRestoredFile(fileName)) {
                    deploymentStore.addUpdatingArtifact(
                            fileName, deploymentStore.getArtifactNameForFile(fileName));
                    deploymentStore.removeArtifactWithFileName(fileName);
                }
            } else {
                // if the file doesn't exists then it is an actual undeployment
                String artifactName = deploymentStore.getArtifactNameForFile(fileName);

                try {
                    // It is not possible to un-deploy either main or fault sequence. Instead when someone
                    // un-deploy one of these updated sequences, It falls back to default sequence.
                    if (fileName.matches(".*main-\\d+\\.\\d+\\.\\d+\\.xml")) {
                        String mainSeqFileName = fileName.substring(fileName.lastIndexOf(File.separator) + 1);

                        // TODO - Improve by adding proper version control
                        if (mainSeqFileName.equals(deploymentStore.getMainSeqLstUpdatedFile())) {
                            resetDefaultSequence(SynapseConstants.MAIN_SEQUENCE_XML, fileName, deploymentStore);
                            deploymentStore.setMainSeqLstUpdatedFile(SynapseConstants.MAIN_SEQUENCE_XML);
                        }
                    } else if (fileName.matches(".*fault-\\d+\\.\\d+\\.\\d+\\.xml")) {
                        String faultSeqFileName = fileName.substring(fileName.lastIndexOf(File.separator) + 1);

                        if (faultSeqFileName.equals(deploymentStore.getFaultSeqLstUpdatedFile())) {
                            resetDefaultSequence(SynapseConstants.FAULT_SEQUENCE_XML, fileName, deploymentStore);
                            deploymentStore.setFaultSeqLstUpdatedFile(SynapseConstants.FAULT_SEQUENCE_XML);
                        }
                    } else {
                        undeploySynapseArtifact(artifactName);
                        deploymentStore.removeArtifactWithFileName(fileName);
                    }
                } catch (SynapseArtifactDeploymentException sade) {
                    log.error("Unable to undeploy the artifact from file : " + fileName, sade);
                    log.info("Restoring the artifact into the file : " + fileName);
                    restoreSynapseArtifact(artifactName);

                } catch (IOException ioe) {
                    log.error("Unable to undeploy the artifact from file : " + fileName, ioe);
                    log.info("Restoring the artifact into the file : " + fileName);
                    restoreSynapseArtifact(artifactName);

                } catch (XMLStreamException xmlse) {
                    log.error("Unable to undeploy the artifact from file : " + fileName, xmlse);
                    log.info("Restoring the artifact into the file : " + fileName);
                    restoreSynapseArtifact(artifactName);
                }
            }
        } else {
            String msg = "Artifact representing the filename "
                    + fileName + " is not deployed on Synapse";
            if (log.isDebugEnabled()) {
                log.debug(msg);
            }
            //throw new DeploymentException(msg);
        }

        if (log.isDebugEnabled()) {
            log.debug("UnDeployment of the synapse artifact from file : "
                    + fileName + " : COMPLETED");
        }
    }

    // We do not support dynamically setting the directory nor the extension
    public void setDirectory(String directory) {}
    public void setExtension(String extension) {}

    /**
     * All synapse artifact deployers MUST implement this method and it handles artifact specific
     * deployment tasks of those artifacts.
     *
     * @param artifactConfig built element representing the artifact to be deployed loaded
     * from the file
     * @param fileName file name from which this artifact is being loaded
     * @param properties Properties associated with the artifact
     * @return String artifact name created by the deployment task
     * 
     * @see AbstractSynapseArtifactDeployer#deploy(
     * org.apache.axis2.deployment.repository.util.DeploymentFileData)
     */
    public abstract String deploySynapseArtifact(OMElement artifactConfig, String fileName,
                                                 Properties properties);

    /**
     * All synapse artifact deployers MUST implement this method and it handles artifact specific
     * update tasks of those artifacts.
     *
     * @param artifactConfig built element representing the artifact to be deployed loaded
     * from the file
     * @param fileName file name from which this artifact is being loaded
     * @param existingArtifactName name of the artifact that was being deployed using
     * the updated file
     * @param properties bag of properties with the additional information
     * @return String artifact name created by the update task
     */
    public abstract String updateSynapseArtifact(OMElement artifactConfig, String fileName,
                                                 String existingArtifactName,
                                                 Properties properties);

    /**
     * All synapse artifact deployers MUST implement this method and it handles artifact specific
     * undeployment tasks of those artifacts.
     *
     * @param artifactName name of the artifact to be undeployed
     *
     * @see AbstractSynapseArtifactDeployer#undeploy(String)
     */
    public abstract void undeploySynapseArtifact(String artifactName);

    /**
     * All synapse artifact deployers MUST implement this method and it handles artifact specific
     * restore tasks of those artifacts upon a failure of an update or undeployment.
     *
     * @param artifactName name of the artifact to be restored
     */
    public abstract void restoreSynapseArtifact(String artifactName);

    protected SynapseConfiguration getSynapseConfiguration() throws DeploymentException {
        Parameter synCfgParam =
                cfgCtx.getAxisConfiguration().getParameter(SynapseConstants.SYNAPSE_CONFIG);
        if (synCfgParam == null) {
            throw new DeploymentException("SynapseConfiguration not found. " +
                    "Are you sure that you are running Synapse?");
        }
        return (SynapseConfiguration) synCfgParam.getValue();
    }

    protected SynapseEnvironment getSynapseEnvironment() throws DeploymentException {
        Parameter synCfgParam =
                cfgCtx.getAxisConfiguration().getParameter(SynapseConstants.SYNAPSE_ENV);
        if (synCfgParam == null) {
            throw new DeploymentException("SynapseEnvironment not found. " +
                    "Are you sure that you are running Synapse?");
        }
        return (SynapseEnvironment) synCfgParam.getValue();
    }

    protected ServerConfigurationInformation getServerConfigurationInformation() 
            throws DeploymentException {
        Parameter serverCfgParam =
                cfgCtx.getAxisConfiguration().getParameter(
                        SynapseConstants.SYNAPSE_SERVER_CONFIG_INFO);
        if (serverCfgParam == null) {
            throw new DeploymentException("SynapseConfigurationInformation not found. " +
                    "Are you sure that you are running Synapse?");
        }
        return (ServerConfigurationInformation) serverCfgParam.getValue();
    }

    protected ServerContextInformation getServerContextInformation()
            throws DeploymentException {
        Parameter serverCtxParam =
                cfgCtx.getAxisConfiguration().getParameter(SynapseConstants.SYNAPSE_SERVER_CTX_INFO);
        if (serverCtxParam == null) {
            throw new DeploymentException("ServerContextInformation not found. " +
                    "Are you sure that you are running Synapse?");
        }
        return (ServerContextInformation) serverCtxParam.getValue();
    }

    protected void writeToFile(OMElement content, String fileName) throws Exception {
        // this is not good, but I couldn't think of a better design :-(
        SynapseArtifactDeploymentStore deploymentStore =
                getSynapseConfiguration().getArtifactDeploymentStore();
        deploymentStore.addRestoredArtifact(fileName);
        OutputStream out = FileUtils.openOutputStream(new File(fileName));
        XMLPrettyPrinter.prettify(content, out);
        out.flush();
        out.close();
    }

    protected void waitForCompletion() {
        long timeout = 2000L;
        Parameter param = cfgCtx.getAxisConfiguration().getParameter("hotupdate.timeout");
        if (param != null && param.getValue() != null) {
            timeout = Long.parseLong(param.getValue().toString());
        }

        try {
            Thread.sleep(timeout);
        } catch (InterruptedException ignored) {

        }
    }

    protected void handleSynapseArtifactDeploymentError(String msg) {
        deployerLog.error(msg);
        throw new SynapseArtifactDeploymentException(msg);
    }

    protected void handleSynapseArtifactDeploymentError(String msg, Exception e) {
        deployerLog.error(msg, e);
        throw new SynapseArtifactDeploymentException(msg, e);
    }

    private void handleDeploymentError(String msg, Exception e, String fileName)
            throws DeploymentException {
        fileName = SynapseArtifactDeploymentStore.getNormalizedAbsolutePath(fileName);
        log.error(msg, e);
        SynapseArtifactDeploymentStore deploymentStore =
                getSynapseConfiguration().getArtifactDeploymentStore();
        if (deploymentStore.isUpdatingArtifact(fileName)) {
            backupFile(new File(fileName));
            log.info("Restoring the existing artifact into the file : " + fileName);
            restoreSynapseArtifact(deploymentStore.getUpdatingArtifactWithFileName(fileName));
            deploymentStore.addArtifact(
                    fileName, deploymentStore.getUpdatingArtifactWithFileName(fileName));
            deploymentStore.removeUpdatingArtifact(fileName);
        }
    }

    private String backupFile(File file) throws DeploymentException {

        //TODO: WSO2 Specific hack to prevent creating backup files in worker nodes
        String isWorker = System.getProperty("workerNode");
        if (isWorker!=null) {
            return "NO_BACKUP_ON_WORKER.INFO";
        }
        String filePath = SynapseArtifactDeploymentStore.getNormalizedAbsolutePath(
                file.getAbsolutePath());

        SynapseArtifactDeploymentStore deploymentStore =
                getSynapseConfiguration().getArtifactDeploymentStore();

        deploymentStore.addBackedUpArtifact(filePath);
        String backupFilePath = filePath + ".back";
        int backupIndex = 0;
        while (backupIndex >= 0) {
            if (new File(backupFilePath).exists()) {
                backupIndex++;
                backupFilePath = filePath + "." + backupIndex + ".back";
            } else {
                backupIndex = -1;
                try {
                    FileUtils.moveFile(file, new File(backupFilePath));
                } catch (IOException e) {
		    log.warn("Error while backing up the artifact: ", e);
                    return "ERROR_WHILE_BACKING_UP_ARTIFACT";
                }
            }
        }
        return backupFilePath;
    }

    private void resetDefaultSequence(String sequenceFileName, String fileName, SynapseArtifactDeploymentStore deploymentStore)
    throws IOException, XMLStreamException {

        String dirpath = fileName.substring(0, fileName.lastIndexOf(File.separator));
        String seqOrgi = dirpath + File.separator + sequenceFileName;

        InputStream in = FileUtils.openInputStream(new File(seqOrgi));

        // construct the xml element from the file, it has to be XML,
        // since all synapse artifacts are XML based
        OMElement element = new StAXOMBuilder(
                StAXUtils.createXMLStreamReader(in)).getDocumentElement();
        Properties properties = new Properties();
        properties.put(SynapseConstants.RESOLVE_ROOT, getSynapseEnvironment()
                .getServerContextInformation()
                .getServerConfigurationInformation().getResolveRoot());

        String existingArtifactName
                = deploymentStore.getArtifactNameForFile(fileName);

        deploymentStore.removeArtifactWithFileName(fileName);

        String artifactName = updateSynapseArtifact(
                element, seqOrgi, existingArtifactName, properties);

        if (artifactName != null) {
            deploymentStore.addArtifact(seqOrgi, artifactName);
        }
    }

    private String updateDefaultSequence(String filename, OMElement element, Properties properties, String lstupdFile,
                                       SynapseArtifactDeploymentStore deploymentStore) {
        String derpath = filename.substring(0, filename.lastIndexOf(File.separator));
        String seqFile = derpath + File.separator + lstupdFile;
        String existingArtifactName
                = deploymentStore.getArtifactNameForFile(seqFile);

        deploymentStore.removeArtifactWithFileName(seqFile);

        String artifactName = updateSynapseArtifact(
                element, filename, existingArtifactName, properties);

        return artifactName;
    }

    public void setCustomLog (String artifactContainerName, String tenantId) {
        if (artifactContainerName == null) {
            customLogContent = null;
            return;
        }
        customLogContent = "[ Deployed From Artifact Container: " + artifactContainerName + " ] ";
    }
}
