/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2010, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.ee.structure;

import static org.jboss.as.ee.EeMessages.MESSAGES;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.jboss.as.server.deployment.Attachments;
import org.jboss.as.server.deployment.DeploymentPhaseContext;
import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.as.server.deployment.DeploymentUnitProcessingException;
import org.jboss.as.server.deployment.DeploymentUnitProcessor;
import org.jboss.as.server.deployment.MountedDeploymentOverlay;
import org.jboss.as.server.deployment.SubDeploymentMarker;
import org.jboss.as.server.deployment.module.ModuleRootMarker;
import org.jboss.as.server.deployment.module.MountHandle;
import org.jboss.as.server.deployment.module.ResourceRoot;
import org.jboss.as.server.deployment.module.TempFileProviderService;
import org.jboss.metadata.ear.spec.EarMetaData;
import org.jboss.metadata.ear.spec.ModuleMetaData;
import org.jboss.metadata.ear.spec.ModuleMetaData.ModuleType;
import org.jboss.vfs.VFS;
import org.jboss.vfs.VFSUtils;
import org.jboss.vfs.VirtualFile;
import org.jboss.vfs.VisitorAttributes;
import org.jboss.vfs.util.SuffixMatchFilter;

/**
 * Deployment processor responsible for detecting EAR deployments and putting setting up the basic structure.
 *
 * @author John Bailey
 * @author Stuart Douglas
 */
public class EarStructureProcessor implements DeploymentUnitProcessor {

    private static final String JAR_EXTENSION = ".jar";
    private static final String WAR_EXTENSION = ".war";
    private static final String SAR_EXTENSION = ".sar";
    private static final String RAR_EXTENSION = ".rar";
    private static final List<String> CHILD_ARCHIVE_EXTENSIONS = new ArrayList<String>();

    static {
        CHILD_ARCHIVE_EXTENSIONS.add(JAR_EXTENSION);
        CHILD_ARCHIVE_EXTENSIONS.add(WAR_EXTENSION);
        CHILD_ARCHIVE_EXTENSIONS.add(SAR_EXTENSION);
        CHILD_ARCHIVE_EXTENSIONS.add(RAR_EXTENSION);
    }

    private static final SuffixMatchFilter CHILD_ARCHIVE_FILTER = new SuffixMatchFilter(CHILD_ARCHIVE_EXTENSIONS, new VisitorAttributes() {

        public boolean isLeavesOnly() {
            return false;
        }
    });

    private static final String DEFAULT_LIB_DIR = "lib";


    public void deploy(final DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException {
        final DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit();
        if (!DeploymentTypeMarker.isType(DeploymentType.EAR, deploymentUnit)) {
            return;
        }

        final ResourceRoot deploymentRoot = phaseContext.getDeploymentUnit().getAttachment(Attachments.DEPLOYMENT_ROOT);
        final VirtualFile virtualFile = deploymentRoot.getRoot();

        //  Make sure we don't index or add this as a module root
        deploymentRoot.putAttachment(Attachments.INDEX_RESOURCE_ROOT, false);
        ModuleRootMarker.mark(deploymentRoot, false);

        String libDirName = DEFAULT_LIB_DIR;
        //its possible that the ear metadata could come for jboss-app.xml
        final boolean appXmlPresent = deploymentRoot.getRoot().getChild("META-INF/application.xml").exists();
        final EarMetaData earMetaData = deploymentUnit.getAttachment(org.jboss.as.ee.structure.Attachments.EAR_METADATA);
        if (earMetaData != null) {
            final String xmlLibDirName = earMetaData.getLibraryDirectory();
            if (xmlLibDirName != null) {
                if (xmlLibDirName.length() == 1 && xmlLibDirName.charAt(0) == '/') {
                    throw MESSAGES.rootAsLibraryDirectory();
                }
                libDirName = xmlLibDirName;
            }
        }

        // Process all the children
        Map<String, MountedDeploymentOverlay> overlays = deploymentUnit.getAttachment(Attachments.DEPLOYMENT_OVERLAY_LOCATIONS);
        try {
            final VirtualFile libDir;
            // process the lib directory
            if (!libDirName.isEmpty()) {
                libDir = virtualFile.getChild(libDirName);
                if (libDir.exists()) {
                    List<VirtualFile> libArchives = libDir.getChildren(CHILD_ARCHIVE_FILTER);
                    for (final VirtualFile child : libArchives) {
                        String relativeName = child.getPathNameRelativeTo(deploymentRoot.getRoot());
                        MountedDeploymentOverlay overlay = overlays.get(relativeName);
                        final MountHandle mountHandle;
                        if(overlay != null) {
                            overlay.remountAsZip(false);
                            mountHandle = new MountHandle(null);
                        } else {
                            final Closeable closable = child.isFile() ? mount(child, false) : null;
                            mountHandle = new MountHandle(closable);
                        }
                        final ResourceRoot childResource = new ResourceRoot(child, mountHandle);
                        if (child.getName().toLowerCase(Locale.ENGLISH).endsWith(JAR_EXTENSION)) {
                            ModuleRootMarker.mark(childResource);
                            deploymentUnit.addToAttachmentList(Attachments.RESOURCE_ROOTS, childResource);
                        }
                    }
                }
            } else {
                libDir = null;
            }
            // scan the ear looking for wars and jars
            final List<VirtualFile> childArchives = new ArrayList<VirtualFile>(virtualFile.getChildren(new SuffixMatchFilter(
                    CHILD_ARCHIVE_EXTENSIONS, new VisitorAttributes() {
                @Override
                public boolean isLeavesOnly() {
                    return false;
                }

                @Override
                public boolean isRecurse(VirtualFile file) {
                    // don't recurse into /lib
                    if (file.equals(libDir)) {
                        return false;
                    }
                    for (String suffix : CHILD_ARCHIVE_EXTENSIONS) {
                        if (file.getName().endsWith(suffix)) {
                            // don't recurse into sub deployments
                            return false;
                        }
                    }
                    return true;
                }
            })));

            // if there is no application.xml then look in the ear root for modules
            if (!appXmlPresent) {
                for (final VirtualFile child : childArchives) {
                    final boolean isWarFile = child.getName().toLowerCase(Locale.ENGLISH).endsWith(WAR_EXTENSION);
                    final boolean isRarFile = child.getName().toLowerCase(Locale.ENGLISH).endsWith(RAR_EXTENSION);
                    this.createResourceRoot(deploymentUnit, child, isWarFile || isRarFile, isWarFile);
                }
            } else {
                final Set<VirtualFile> subDeploymentFiles = new HashSet<VirtualFile>();
                // otherwise read from application.xml
                for (final ModuleMetaData module : earMetaData.getModules()) {

                    if(module.getFileName().endsWith(".xml")) {
                        throw MESSAGES.unsupportedModuleType(module.getFileName());
                    }

                    final VirtualFile moduleFile = virtualFile.getChild(module.getFileName());
                    if (!moduleFile.exists()) {
                        throw MESSAGES.cannotProcessEarModule(virtualFile, module.getFileName());
                    }

                    if (libDir != null) {
                        VirtualFile moduleParentFile = moduleFile.getParent();
                        if (moduleParentFile != null) {
                            if (libDir.equals(moduleParentFile)) {
                                throw MESSAGES.earModuleChildOfLibraryDirectory(libDirName, module.getFileName());
                            }
                        }
                    }

                    // maintain this in a collection of subdeployment virtual files, to be used later
                    subDeploymentFiles.add(moduleFile);

                    final boolean webArchive = module.getType() == ModuleType.Web;
                    final ResourceRoot childResource = this.createResourceRoot(deploymentUnit, moduleFile, true, webArchive);
                    childResource.putAttachment(org.jboss.as.ee.structure.Attachments.MODULE_META_DATA, module);

                    if (!webArchive) {
                        ModuleRootMarker.mark(childResource);
                    }

                    final String alternativeDD = module.getAlternativeDD();
                    if (alternativeDD != null && alternativeDD.trim().length() > 0) {
                        final VirtualFile alternateDeploymentDescriptor = deploymentRoot.getRoot().getChild(alternativeDD);
                        if (!alternateDeploymentDescriptor.exists()) {
                            throw MESSAGES.alternateDeploymentDescriptor(alternateDeploymentDescriptor, moduleFile);
                        }
                        switch (module.getType()) {
                            case Client:
                                childResource.putAttachment(org.jboss.as.ee.structure.Attachments.ALTERNATE_CLIENT_DEPLOYMENT_DESCRIPTOR, alternateDeploymentDescriptor);
                                break;
                            case Connector:
                                childResource.putAttachment(org.jboss.as.ee.structure.Attachments.ALTERNATE_CONNECTOR_DEPLOYMENT_DESCRIPTOR, alternateDeploymentDescriptor);
                                break;
                            case Ejb:
                                childResource.putAttachment(org.jboss.as.ee.structure.Attachments.ALTERNATE_EJB_DEPLOYMENT_DESCRIPTOR, alternateDeploymentDescriptor);
                                break;
                            case Web:
                                childResource.putAttachment(org.jboss.as.ee.structure.Attachments.ALTERNATE_WEB_DEPLOYMENT_DESCRIPTOR, alternateDeploymentDescriptor);
                                break;
                            case Service:
                                throw MESSAGES.unsupportedModuleType(module.getFileName());

                        }
                    }
                }
                // now check the rest of the archive for any other jar/sar files
                for (final VirtualFile child : childArchives) {
                    if (subDeploymentFiles.contains(child)) {
                        continue;
                    }
                    final String fileName = child.getName().toLowerCase(Locale.ENGLISH);
                    if (fileName.endsWith(SAR_EXTENSION) || fileName.endsWith(JAR_EXTENSION)) {
                        this.createResourceRoot(deploymentUnit, child, false, false);
                    }
                }
            }

        } catch (IOException e) {
            throw MESSAGES.failedToProcessChild(e, virtualFile);
        }
    }

    private static Closeable mount(VirtualFile moduleFile, boolean explode) throws IOException {
        return explode ? VFS.mountZipExpanded(moduleFile, moduleFile, TempFileProviderService.provider())
                : VFS.mountZip(moduleFile, moduleFile, TempFileProviderService.provider());
    }

    /**
     * Creates a {@link ResourceRoot} for the passed {@link VirtualFile file} and adds it to the list of {@link ResourceRoot}s
     * in the {@link DeploymentUnit deploymentUnit}
     *
     * @param deploymentUnit      The deployment unit
     * @param file                The file for which the resource root will be created
     * @param markAsSubDeployment If this is true, then the {@link ResourceRoot} that is created will be marked as a subdeployment
     *                            through a call to {@link SubDeploymentMarker#mark(org.jboss.as.server.deployment.module.ResourceRoot)}
     * @param explodeDuringMount  If this is true then the {@link VirtualFile file} will be exploded during mount,
     *                            while creating the {@link ResourceRoot}
     * @return Returns the created {@link ResourceRoot}
     * @throws IOException
     */
    private ResourceRoot createResourceRoot(final DeploymentUnit deploymentUnit, final VirtualFile file, final boolean markAsSubDeployment, final boolean explodeDuringMount) throws IOException {
        final boolean war = file.getName().toLowerCase(Locale.ENGLISH).endsWith(WAR_EXTENSION);
        final Closeable closable = file.isFile() ? mount(file, explodeDuringMount) : exportExplodedWar(war, file, deploymentUnit);
        final MountHandle mountHandle = new MountHandle(closable);
        final ResourceRoot resourceRoot = new ResourceRoot(file, mountHandle);
        deploymentUnit.addToAttachmentList(Attachments.RESOURCE_ROOTS, resourceRoot);
        if (markAsSubDeployment) {
            SubDeploymentMarker.mark(resourceRoot);
        }
        if (war) {
            resourceRoot.putAttachment(Attachments.INDEX_RESOURCE_ROOT, false);
        }
        return resourceRoot;
    }

    private Closeable exportExplodedWar(final boolean war, final VirtualFile file, final DeploymentUnit deploymentUnit) throws IOException {
        if (isExplodedWarInArchiveEar(war, file, deploymentUnit)) {
            File warContent = file.getPhysicalFile();
            VFSUtils.recursiveCopy(file, warContent.getParentFile());
            return VFS.mountReal(warContent, file);
        }
        return null;
    }

    private boolean isExplodedWarInArchiveEar(final boolean war, final VirtualFile file, final DeploymentUnit deploymentUnit) {
        return war && !file.isFile() && deploymentUnit.hasAttachment(Attachments.DEPLOYMENT_CONTENTS) && deploymentUnit.getAttachment(Attachments.DEPLOYMENT_CONTENTS).isFile();
    }

    public void undeploy(DeploymentUnit context) {
        final List<ResourceRoot> children = context.removeAttachment(Attachments.RESOURCE_ROOTS);
        if (children != null) {
            for (ResourceRoot childRoot : children) {
                VFSUtils.safeClose(childRoot.getMountHandle());
            }
        }
    }
}
