/*
 * 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.jetspeed.maven.plugins;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.jetspeed.maven.utils.Artifacts;
import org.apache.jetspeed.tools.deploy.DeployFactory;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;

/**
 * DeployerMojo is a generic Jetspeed Portal artifacts en resources deployer.
 * 
 * @version $Id: DeployMojo.java 1174669 2011-09-23 11:31:47Z ate $
 * @goal deploy
 */
public class DeployMojo extends AbstractMojo
{
    private static final String DEPLOY_FACTORY_CLASS_NAME = "org.apache.jetspeed.tools.deploy.JetspeedDeployFactory";

    /**
     * Support only Tomcat 6.0.33+. The profile attribute is
     * typically set in the plugin configurations using the following tags:
     *
     * <profile>tomcat${org.apache.jetspeed.catalina.version.major}</profile>
     * or
     * <profile>tomcat${tomcat.version.major}</profile>
     *
     * The supported values for the org.apache.jetspeed.catalina.version.major
     * property are '6' and '7'. If unset, the profile value will be
     * 'tomcat' or 'tomcatnull', which will imply the default tomcat support.
     *
     * The supported values for the tomcat.version.major are '6' and '7'.
     * 
     * The current implementation makes no distinction between either version (anymore).
     */
    private static final String PROFILE_TOMCAT = "tomcat";
    private static final String PROFILE_TOMCAT_NULL = "tomcatnull";
    private static final String PROFILE_TOMCAT6 = "tomcat6";
    private static final String PROFILE_TOMCAT7 = "tomcat7";
    private static final String DEFAULT_PROFILE = PROFILE_TOMCAT6; 

    public static class Deployment
    {
        private String artifact;
        private String file;
        private String destination;
        private String targetName;
        private Boolean stripVersionId;
        private String delete;
        private Boolean expand;
        private boolean expandKeepExt;
        private Boolean infuse;
        private Boolean infusionStripLoggers;
        private String infusionForcedVersion;
    }
    
    private static class DeploymentObject
    {
        private String type;
        private Deployment deployment;        
        private File src;
        private File targetDir;
        private Artifact artifact;
        private String contextName;
    }
    
    public static class Edit
    {
        private String destination;
        private String targetName;
        private String editorClassName;
        private boolean required = true;
    }
    
    /**
     * The target base directory.
     * @parameter
     * @required
     */
    private String targetBaseDir;
    
    /**
     * The name of the portal web application
     * @parameter expression="jetspeed"
     */
    private String portalName;
    
    /**
     * @parameter expression="false";
     */
    private Boolean infusionStripLoggers;
    
    /**
     * @parameter
     */
    private String infusionForcedVersion;
    
    /**
     * @parameter
     * @required
     */
    private Deployment[] deployments;
    
    /**
     * @parameter expression="${localRepository}"
     * @required
     * @readonly
     */
    protected ArtifactRepository localRepository;
    
    /**
     * @parameter
     */
    private Map destinations;

    /** @parameter expression="${plugin.artifacts}" */
    private ArrayList pluginArtifacts;
    
    /**
     * @parameter
     */
    private String profile;

    /**
     * @parameter
     */
    private Edit[] edits;

    private Artifacts artifacts;
    
    /* (non-Javadoc)
     * @see org.apache.maven.plugin.Mojo#execute()
     */
    public void execute() throws MojoExecutionException, MojoFailureException
    {
        HashMap destMap = new HashMap();
        if (destinations != null)
        {
            destMap.putAll(destinations);
        }

       	initDefaultDestinations(destMap, profile);
        
        File targetBaseDir = new File(this.targetBaseDir);
        if (targetBaseDir.exists() && targetBaseDir.isFile())
        {
            throw new MojoExecutionException("targetBaseDir "+this.targetBaseDir+" points to a file, not a directory");
        }
        File portalDeployDir = new File(targetBaseDir,(String)destMap.get("deploy"));                
        File localPortalDeployDir = new File(targetBaseDir, (String)destMap.get("local"));                
        
        artifacts = new Artifacts(pluginArtifacts);
        
        boolean infusion = false;
        
        List objects = new ArrayList();
                
        for (int i = 0; i < deployments.length; i++)
        {
            DeploymentObject dobj = new DeploymentObject();
            dobj.deployment = deployments[i];
            if (dobj.deployment.artifact != null)
            {
                dobj.artifact = artifacts.get(dobj.deployment.artifact);
                if (dobj.artifact == null)
                {
                    throw new MojoExecutionException("Artifact "+dobj.deployment.artifact+" dependency not defined");
                }
                dobj.src = dobj.artifact.getFile();
                if (dobj.src == null)
                {
                    String location = localRepository.pathOf(dobj.artifact);
                    if (location == null)
                    {
                        throw new MojoExecutionException("Artifact "+dobj.deployment.artifact+" could not be found in local repository");
                    }
                    dobj.src = new File(localRepository.getBasedir(), location);
                }
                dobj.type = dobj.artifact.getType();
            }
            else if (dobj.deployment.file != null)
            {
                dobj.src = new File(dobj.deployment.file);
                if (!dobj.src.exists() || !dobj.src.isFile())
                {
                    throw new MojoExecutionException("Deployment file "+dobj.deployment.file+" not found");
                }
                int index = dobj.deployment.file.lastIndexOf(".");
                if (index > -1 && index < dobj.deployment.file.length()-1)
                {
                    dobj.type = dobj.deployment.file.substring(index+1);
                }
            }
            else
            {
                throw new MojoExecutionException("A deployment requires an artifact of file specification");
            }
            
            if ("war".equals(dobj.type))
            {
                dobj.deployment.destination = getValue(dobj.deployment.destination,"war");
                if ("war".equals(dobj.deployment.destination))
                {
                    dobj.deployment.expand = getValue(dobj.deployment.expand, Boolean.FALSE);
                    dobj.deployment.infuse = getValue(dobj.deployment.infuse, Boolean.FALSE);
                    dobj.deployment.infusionStripLoggers = getValue(dobj.deployment.infusionStripLoggers, infusionStripLoggers);
                    dobj.deployment.infusionForcedVersion = getValue(dobj.deployment.infusionForcedVersion, infusionForcedVersion);
                }
                else
                {
                    dobj.deployment.expand = Boolean.FALSE;
                    dobj.deployment.infuse = Boolean.FALSE;
                }
            }
            else
            {
                dobj.deployment.destination = getValue(dobj.deployment.destination, "jar".equals(dobj.type) ? "lib" : null);
                dobj.deployment.expand = getValue(dobj.deployment.expand, Boolean.FALSE);
                dobj.deployment.infuse = Boolean.FALSE;
            }
            if (destMap.get(dobj.deployment.destination) == null)
            {
                throw new MojoExecutionException("Unknown or unspecified deployment destination: \""+dobj.deployment.destination+"\"");
            }
            
            if (dobj.deployment.infuse.booleanValue())
            {
                infusion = true;
            }
            boolean portalDeploy = "deploy".equals(dobj.deployment.destination);
            boolean localPortalDeploy = "local".equals(dobj.deployment.destination);
            
            if (dobj.deployment.targetName == null)
            {
                if (dobj.artifact != null)
                {
                    Boolean defaultStripVersionId = "war".equals(dobj.type) || "war".equals(dobj.deployment.destination) || portalDeploy || localPortalDeploy ? Boolean.TRUE : Boolean.FALSE;
                    if (getValue(dobj.deployment.stripVersionId, defaultStripVersionId).booleanValue())
                    {
                        dobj.deployment.targetName = dobj.artifact.getArtifactId() + "." + dobj.artifact.getType();
                    }
                    else
                    {
                        dobj.deployment.targetName = dobj.artifact.getArtifactId() + "-"+ dobj.artifact.getVersion() + "." + dobj.artifact.getType();
                    }
                }
                else
                {
                    dobj.deployment.targetName = dobj.src.getName();
                }
                if (dobj.deployment.expand.booleanValue() && !dobj.deployment.expandKeepExt)
                {
                    int index = dobj.deployment.targetName.lastIndexOf(".");
                    if (index > -1)
                    {
                        dobj.deployment.targetName = dobj.deployment.targetName.substring(0, index);
                    }
                }
            }
            if (dobj.deployment.infuse.booleanValue())
            {                    
                if (dobj.deployment.expand.booleanValue())
                {
                    dobj.contextName = dobj.deployment.targetName;
                }
                else
                {
                    int index = dobj.deployment.targetName.lastIndexOf(".");
                    if (index > -1)
                    {
                        dobj.contextName = dobj.deployment.targetName.substring(0, index);
                    }
                    else
                    {
                        dobj.contextName = dobj.deployment.targetName;
                    }
                }
            }
            if (portalDeploy)
            {
                dobj.targetDir = portalDeployDir;
            }
            else if (localPortalDeploy)
            {
                dobj.targetDir = localPortalDeployDir;
            }
            else
            {
                dobj.targetDir = new File(targetBaseDir, (String)destMap.get(dobj.deployment.destination));
            }
            
            objects.add(dobj);
            
        }
        
        DeployFactory deployFactory = null;
        if (infusion)
        {                
            if (deployFactory == null)
            {
                try
                {
                    Class dfClass = Class.forName(DEPLOY_FACTORY_CLASS_NAME, true, Thread.currentThread().getContextClassLoader());
                    deployFactory = (DeployFactory) dfClass.newInstance();
                }
                catch (Exception e)
                {
                    throw new MojoExecutionException("Cannot find or load DeployFactory class "+ DEPLOY_FACTORY_CLASS_NAME, e);
                }
            }
        }
        
        String deploySelection = System.getProperty("deploy");
        if (deploySelection != null)
        {
            List selection = new ArrayList();
            StringTokenizer st = new StringTokenizer(deploySelection,",;");
            while (st.hasMoreTokens())
            {
                String destination = st.nextToken();
                String name = null;
                int split = destination.indexOf(":");
                if (split > 0)
                {
                    name = destination.substring(split+1);
                    destination = destination.substring(0, split);
                }
                for (Iterator iter = objects.iterator(); iter.hasNext(); )
                {
                    DeploymentObject dobj = (DeploymentObject)iter.next();
                    if (dobj.deployment.destination.equals(destination))
                    {
                        if (name == null || name.equals(dobj.deployment.targetName) && !(selection.contains(dobj)))
                        {
                            selection.add(dobj);
                        }
                    }
                }
            }
            objects = selection;
        }
        
        if (objects.size() > 0)
        {
            getLog().info("Deploying to targetBaseDir "+targetBaseDir.getAbsolutePath());
        }
        
        for (Iterator iter = objects.iterator(); iter.hasNext(); )
        {
            DeploymentObject dobj = (DeploymentObject)iter.next();

            String infoPostFix = "";
            if (dobj.deployment.infuse.booleanValue())
            {
                infoPostFix += " (infused";
                if (dobj.deployment.expand.booleanValue())
                {
                    infoPostFix += ", expanded";
                }
                infoPostFix += ")";
            }
            else if (dobj.deployment.expand.booleanValue())
            {
                infoPostFix += " (expanded)";
            }
            
            getLog().info("  deploying to "+dobj.deployment.destination+": "+dobj.deployment.targetName + infoPostFix);
            
            checkMkdirs(dobj.targetDir);
            
            if (dobj.deployment.delete != null)
            {
                File delete = new File(dobj.targetDir, dobj.deployment.delete);

                if (delete.exists())
                {
                    if (delete.isDirectory())
                    {
                        if (!rmdir(delete))
                        {
                            throw new MojoExecutionException("Failed to delete directory "+ delete.getAbsolutePath());
                        }
                    }
                    else
                    {
                        if (!delete.delete())
                        {
                            throw new MojoExecutionException("Failed to delete file "+ delete.getAbsolutePath());
                        }
                    }
                }
            }
            
            File target = new File(dobj.targetDir, dobj.deployment.targetName);
            
            if (target.exists())
            {
                if (dobj.deployment.expand.booleanValue() && !target.isDirectory())
                {
                    throw new MojoExecutionException("Resolved target directory "+ target.getAbsolutePath()+" points to a file");
                }
                else if (!dobj.deployment.expand.booleanValue() && target.isDirectory())
                {
                    throw new MojoExecutionException("Resolved target file "+ target.getAbsolutePath()+" points to a directory");
                }
            }
            
            if ("lib".equals(dobj.deployment.destination) && dobj.artifact != null)
            {
                deleteMatchingFiles(dobj.targetDir, dobj.artifact.getArtifactId(), dobj.artifact.getType());
            }

            if (dobj.deployment.infuse.booleanValue())
            {                
                File tmpTarget = null;
                try
                {
                    tmpTarget = File.createTempFile(dobj.src.getName()+".infused-", "");
                    tmpTarget.deleteOnExit();
                }
                catch (IOException e)
                {
                    throw new MojoExecutionException("Failed to create temporary file",e);
                }
                try
                {
                    deployFactory.getInstance(dobj.src.getAbsolutePath(), tmpTarget.getAbsolutePath(), dobj.contextName, dobj.deployment.infusionStripLoggers.booleanValue(), dobj.deployment.infusionForcedVersion);
                    dobj.src = tmpTarget;
                }
                catch (Exception e)
                {
                    throw new MojoExecutionException("Failed to infuse "+dobj.src.getAbsolutePath(),e);
                }
            }
            if (dobj.deployment.expand.booleanValue())
            {
                try
                {
                    if (target.exists() && !rmdir(target))
                    {
                        throw new MojoExecutionException("Failed to delete directory "+ target.getAbsolutePath());
                    }
                    expandWar(dobj.src, target);
                }
                catch (IOException e)
                {
                    throw new MojoExecutionException("Failed to expand infused war",e);
                }
            }
            else
            {
                try
                {
                    copyFile(dobj.src, target);
                }
                catch (Exception e)
                {
                    throw new MojoExecutionException("Failed to copy "+dobj.src.getAbsolutePath() +" to "+target.getAbsolutePath(),e);
                }
            }
        }

        if (edits != null)
        {
            for (int i = 0; i < edits.length; i++)
            {
                Edit edit = edits[i];
                
                // validate edit
                if (edit.destination == null)
                {
                    throw new MojoExecutionException("Required edit destination not specified");
                }
                if (edit.targetName == null)
                {
                    throw new MojoExecutionException("Required edit targetName not specified");
                }
                if (edit.editorClassName == null)
                {
                    throw new MojoExecutionException("Required edit editorClassName not specified");
                }
                
                // instantiate editor class and find method that takes a single
                // file attribute to edit
                Class<?> editorClass = null;
                try
                {
                    editorClass = Class.forName(edit.editorClassName);
                }
                catch (ClassNotFoundException cnfe)
                {
                    throw new MojoExecutionException("Cannot load editor class: "+cnfe, cnfe);
                }
                try
                {
                    editorClass.getConstructor(new Class<?>[0]);
                }
                catch (NoSuchMethodException nsme)
                {
                    throw new MojoExecutionException("Cannot find default constructor for "+editorClass);                    
                }
                Method editorMethod = null;
                Method [] editorMethods = editorClass.getDeclaredMethods();
                for (int j = 0; ((editorMethod == null) && (j < editorMethods.length)); j++)
                {
                    Method method = editorMethods[j];
                    Type [] parameterTypes = method.getGenericParameterTypes();
                    Type returnType = method.getGenericReturnType();
                    int modifiers = method.getModifiers();
                    if ((parameterTypes.length == 1) && (parameterTypes[0] == File.class) && (returnType == Void.TYPE) && Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers))
                    {
                        editorMethod = method;
                    }
                }
                if (editorMethod == null)
                {
                    throw new MojoExecutionException("Cannot find editor method in "+editorClass+" with signature: public void method(java.io.File file)");
                }
                
                // validate file destination and targetName
                File targetDir = new File(targetBaseDir, (String)destMap.get(edit.destination));
                File editTargetFile = new File(targetDir, edit.targetName);
                if (!editTargetFile.isFile() || !editTargetFile.canRead() || !editTargetFile.canWrite())
                {
                    String msg = "Cannot find, read, or write target file to edit: "+editTargetFile;
                    if (edit.required)
                    {
                        throw new MojoExecutionException(msg);
                    }
                    else
                    {
                        getLog().warn(msg);
                        continue;
                    }
                }
                
                // instantiate editor and invoke editor to edit target file
                try
                {
                    Object editorInstance = editorClass.newInstance();
                    editorMethod.invoke(editorInstance, editTargetFile);

                    getLog().info("  editing in "+edit.destination+": "+edit.targetName+" using "+editorClass.getSimpleName()+"."+editorMethod.getName()+"()");
                }
                catch (InvocationTargetException ite)
                {
                    throw new MojoExecutionException("Exception thrown by editor method: "+editorClass.getName()+"."+editorMethod.getName()+"("+editTargetFile+"): "+ite.getTargetException(), ite.getTargetException());
                }
                catch (Exception e)
                {
                    throw new MojoExecutionException("Failed to instantiate or invoke editor method: "+editorClass.getName()+"."+editorMethod.getName()+"("+editTargetFile+"): "+e, e);
                }
            }
        }
    }

	private static String getValue(String value, String defaultValue)
    {
        return value != null ? value : defaultValue;
    }
    
    private static Boolean getValue(Boolean value, Boolean defaultValue)
    {
        return value != null ? value : defaultValue;
    }
    
    private static void checkMkdirs(File dir) throws MojoExecutionException
    {
        if (dir.exists())
        {
            if (dir.isFile())
            {
                throw new MojoExecutionException("Target "+dir.getAbsolutePath()+" points to a file, not a directory");
            }
        }
        else
        {
            dir.mkdirs();
        }
    }
    
    private static final void deleteMatchingFiles(File dir, final String namePrefix, final String ext)
    {
        File[] matched = dir.listFiles(new FileFilter(){

            public boolean accept(File file)
            {
                String name = file.getName();
                int index = name.lastIndexOf(".");
                return index > -1 && file.isFile() && ext.equals(name.substring(index+1)) && name.startsWith(namePrefix);
            }});
        
        if (matched != null)
        {
            for (int i = 0; i < matched.length; i++ )
            {
                matched[i].delete();
            }
        }
    }
    
    private void copyFile(File src, File dest) throws IOException
    {
        FileChannel srcChannel = null;
        FileChannel dstChannel = null;

        try
        {
            dest.createNewFile();
            srcChannel = new FileInputStream(src).getChannel();
            dstChannel = new FileOutputStream(dest).getChannel();
            dstChannel.transferFrom(srcChannel, 0, srcChannel.size());
            srcChannel.close();
            dstChannel.close();
        }
        finally
        {
            if ( srcChannel != null && srcChannel.isOpen() )
            {
                try
                {
                    srcChannel.close();
                }
                catch (Exception e)
                {
                }
            }
            if ( dstChannel != null && dstChannel.isOpen() )
            {
                try
                {
                    dstChannel.close();
                }
                catch (Exception e)
                {
                }
            }
        }
    }

    private static final boolean rmdir(File dir)
    {    
        if (dir.isDirectory())
        {
            String[] children = dir.list();
            for (int i = 0; i < children.length; i++)
            {
                boolean success = rmdir(new File(dir, children[i]));
                if (!success)
                {
                    return false;
                }
            }
        }
        // The directory is now empty so delete it OR it is a plain file
        return dir.delete();
    }

    private static void expandWar( File file, File target) throws IOException
    {
        JarFile jarFile = new JarFile(file);

        if (!target.exists())
        {
            target.mkdirs();
        }

        Enumeration entries = jarFile.entries();
        while (entries.hasMoreElements())
        {
            JarEntry jarEntry = (JarEntry) entries.nextElement();
            String name = jarEntry.getName();
            if (jarEntry.isDirectory())
            {
                new File(target, name).mkdir();
            }
            else
            {
                copyEntryToFile(jarFile, target, jarEntry);
            }
        }
    }

    /**
     * <p>
     * copyEntryToFile
     * </p>
     * 
     * @param jarFile
     * @param target
     * @param jarEntry
     * @throws IOException
     * @throws FileNotFoundException
     */
    private static void copyEntryToFile( JarFile jarFile, File target, JarEntry jarEntry ) throws IOException,
            FileNotFoundException
    {
        String name = jarEntry.getName();
        File file = new File(target, name);
        if (!file.getParentFile().exists())
        {
            file.getParentFile().mkdirs();
        }
        file.createNewFile();

        InputStream is = null;
        OutputStream os = null;
        try
        {
            is = jarFile.getInputStream(jarEntry);
            os = new FileOutputStream(file);

            byte[] buf = new byte[1024];
            int len;
            while ((len = is.read(buf)) > 0)
            {
                os.write(buf, 0, len);
            }
        }
        finally
        {
            if (is != null)
            {
                is.close();
            }

            if (os != null)
            {
                os.close();
            }
        }
    }
    
    /**
     * Initialize default destinations for a certain profile.
     */
    private void initDefaultDestinations(Map destMap, String profile) throws MojoFailureException
    {
        // validate profile settings
        if ((profile == null) || profile.equals(PROFILE_TOMCAT_NULL) || profile.equals(PROFILE_TOMCAT))
        {
            profile = DEFAULT_PROFILE;
        }
    	else if (!(profile.equals(PROFILE_TOMCAT6) || profile.equals(PROFILE_TOMCAT7)))
        {
    		throw new MojoFailureException("Cannot handle profile '" + profile + "', use '" + PROFILE_TOMCAT6 + "' or '" + PROFILE_TOMCAT7 + "'");
    	}

        if (!destMap.containsKey("system"))
        {
            destMap.put("system","lib");
        }
        if (!destMap.containsKey("lib"))
        {
            destMap.put("lib","lib");
        }
        if (!destMap.containsKey("war"))
        {
            destMap.put("war", "webapps");
        }
        if (!destMap.containsKey("deploy"))
        {
            destMap.put("deploy", ((String)destMap.get("war"))+"/"+portalName+"/WEB-INF/deploy");
        }
        if (!destMap.containsKey("local"))
        {
            destMap.put("local", ((String)destMap.get("deploy"))+"/local");
        }
        if (!destMap.containsKey("server-conf"))
        {
            destMap.put("server-conf", "conf");
        }
        if (!destMap.containsKey("server-lib"))
        {
            destMap.put("server-lib", "lib");
        }
    }
}	
