/*
 * 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.appclient;

import com.sun.corba.ee.impl.codegen.Attribute;
import com.sun.enterprise.appclient.jws.boot.JWSACCMain;
import com.sun.enterprise.deployment.Application;
import com.sun.enterprise.deployment.ApplicationClientDescriptor;
import com.sun.enterprise.deployment.archivist.Archivist;
import com.sun.enterprise.deployment.archivist.ApplicationArchivist;
import com.sun.enterprise.deployment.backend.J2EEModuleExploder;
import com.sun.enterprise.deployment.BundleDescriptor;
import com.sun.enterprise.deployment.deploy.shared.AbstractArchive;
import com.sun.enterprise.deployment.deploy.shared.FileArchive;
import com.sun.enterprise.deployment.RootDeploymentDescriptor;
import com.sun.enterprise.deployment.util.ModuleDescriptor;
import com.sun.enterprise.loader.EJBClassLoader;
import com.sun.enterprise.util.io.FileUtils;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.FilenameFilter;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.enterprise.deploy.shared.ModuleType;
import javax.print.attribute.AttributeSet;
import org.xml.sax.SAXParseException;

/**
 * Represents an app client that is nested inside an enterprise app.
 *
 * Note that this could be either an undeployed ear that contains one or more
 * embedded app clients or the generated jar file from the back-end that 
 * intentionally resembles an application archive because of other files that
 * had to be packaged with the app client.
 *
 * @author tjquinn
 */
public class NestedAppClientInfo extends AppClientInfo {

    private Application appDesc = null;
    
    /** which of possibly several app clients in the app the user chose */
    private ApplicationClientDescriptor selectedAppClientDescriptor = null;

    /** display name specified (if any) on the command line to use in selecting the desired client main class */
    private String displayNameFromCommandLine;

    private final PropertyChangeListener policyChangeListener;
    
    public NestedAppClientInfo(
            boolean isJWS, Logger logger, File archive, 
            Archivist archivist, String mainClassFromCommandLine, 
            String displayNameFromCommandLine, PropertyChangeListener policyChangeListener) {
        super(isJWS, logger, archive, archivist, mainClassFromCommandLine);
        this.policyChangeListener = policyChangeListener;
        this.displayNameFromCommandLine = displayNameFromCommandLine;
    }

    /**
     *Reports which app client embedded in the application archive is the
     *one the user has selected using either the main class or display name
     *arguments from the command line.
     *@return the app client descriptor for the user-selected app client
     */
    protected ApplicationClientDescriptor getAppClient(Archivist archivist) {

        if (selectedAppClientDescriptor != null) {
            return selectedAppClientDescriptor;
        }

        Application app = Application.class.cast(archivist.getDescriptor());

        /*
         *There could be one or more app clients embedded in the enterprise app
         *in the archive.  Choose which one to run based on the user's 
         *command-line input.
         */
        Set<ApplicationClientDescriptor> embeddedAppClients = 
            (Set<ApplicationClientDescriptor>) 
                app.getApplicationClientDescriptors();

        /*
         *Make sure the application module contains at least one app client.
         */
        if (embeddedAppClients.size() == 0) {
            throw new IllegalArgumentException(
                localStrings.getString("appclient.noEmbeddedAppClients"));
        }

        /*
         *If there is exactly one app client in the ear, then that's the app
         *client to use.
         */
        if (embeddedAppClients.size() == 1) {
            selectedAppClientDescriptor = useFirstEmbeddedAppClient(
                    embeddedAppClients, mainClassFromCommandLine);
        } else {
            selectedAppClientDescriptor = chooseFromEmbeddedAppClients(
                    embeddedAppClients, mainClassFromCommandLine, 
                    displayNameFromCommandLine);

            /*
             *Make sure that we've selected an app client.
             */
            if (selectedAppClientDescriptor == null) {
                if (mainClassFromCommandLine != null) {
                    throw new IllegalArgumentException(localStrings.getString("appclient.noMatchingClientUsingMainClass", mainClassFromCommandLine));
                } else {
                    throw new IllegalArgumentException(localStrings.getString("appclient.noMatchingClientUsingDisplayName", displayNameFromCommandLine));
                }
            }
        }
        return selectedAppClientDescriptor;
    }

    private ApplicationClientDescriptor chooseFromEmbeddedAppClients(
            Set<ApplicationClientDescriptor> embeddedAppClients, 
            String mainClassFromCommandLine, 
            String displayNameFromCommandLine) {
        ApplicationClientDescriptor result = null;
        
        /*
         *There are at least two app clients embedded in the ear.
         *
         *To remain compatible with earlier releases the logic below
         *exits the loop immediately upon finding a matching app client
         *using the user-provided main class name.  If there are other
         *app clients with the same main class those are ignored.
         *
         *On the other hand, if the user specified the target display name 
         *then the logic below makes sure that exactly one app client
         *has that display name.  
         *
         */
        for (ApplicationClientDescriptor candidate : embeddedAppClients) {
           /*
            *If the user specified a main class name, use that value to
            *match against the candiate.
            */
           if (mainClassFromCommandLine != null) {
               if (candidate.getMainClassName().equals(mainClassFromCommandLine)) {
                   result = candidate;
                   /*
                    *Because the main class name is used as the criteria,
                    *exit the loop as soon as one matching app client if found.
                    */
                   break;
               }
           } else {
               /*
                *We know at this point that the user provided a display name.
                */
               if (candidate.getName().equals(displayNameFromCommandLine)) {
                   /*
                    *Make sure no other candidate already matched the
                    *target display name.
                    */
                   if (result == null) {
                       result = candidate;
                       /*
                        *Because the display name is used as the matching
                        *criteria, continue the loop to make sure there are
                        *not multiple app clients with the same display name
                        */
                   } else {
                       throw new IllegalArgumentException(localStrings.getString("appclient.duplicate_display_name", displayNameFromCommandLine));
                   }
               }
           }
        }
        return result;
    }
        
    private ApplicationClientDescriptor useFirstEmbeddedAppClient(Set<ApplicationClientDescriptor> embeddedAppClients, String mainClassNameFromCommandLine) {
        ApplicationClientDescriptor result = null;
        
        /*
         *If the size is 1 then there is sure to be a non-null .next.
         *Still, may as well be sure.
         */
        Iterator<ApplicationClientDescriptor> it = embeddedAppClients.iterator();
        if ( ! it.hasNext()) {
            throw new IllegalStateException(localStrings.getString("appclient.unexpectedEndOfEmbeddedClients"));
        }

        result = embeddedAppClients.iterator().next();

        /*
         *If, in addition, the user specified a main class on the command
         *line, then use the user's class name as the main class name, rather
         *than the class specified by the Main-Class attribute in the
         *app client archive.  This allows the user to override the Main-Class
         *setting in the app client's manifest.
         */
        if (mainClassNameFromCommandLine != null) {
            result.setMainClassName(mainClassNameFromCommandLine);
        }
        return result;
    }

    /**
     *Expands the contents of the source archive into a temporary
     *directory, using the same format as backend server expansions.
     *@param file an archive file to be expanded
     *@return an opened FileArchive for the expanded directory archive
     *@exception IOException in case of errors during the expansion
     */
    protected AbstractArchive expand(File file) 
        throws IOException, Exception {

        File tmpDir = createTmpArchiveDir(file);
        _logger.fine("Expanding original archive " + file.getAbsolutePath() + 
                " into " + tmpDir.getAbsolutePath());

        // first explode the top level jar
        J2EEModuleExploder.explodeJar(file, tmpDir);

        // now we need to load the application standard deployment descriptor.
        FileArchive appArchive = new FileArchive();
        appArchive.open(tmpDir.getAbsolutePath());

        ApplicationArchivist archivist = new ApplicationArchivist();
        if (archivist.hasStandardDeploymentDescriptor(appArchive)) {
            appDesc = (Application) 
            archivist.readStandardDeploymentDescriptor(appArchive);
        } else {
            appDesc = Application.createApplication(appArchive,true);
        }
        
        // explode the sub modules, skipping the ones that do not exist since
        // the generated appclient jar files do not contain web content
        Iterator<ModuleDescriptor> bundles = appDesc.getModules();
        while (bundles.hasNext()) {

            ModuleDescriptor bundle = bundles.next();
            
            String moduleName = bundle.getArchiveUri();
            File srcArchive = new File(tmpDir, moduleName);

            if (srcArchive.exists()) {
                String massagedModuleName =  
                    FileUtils.makeFriendlyFilename(moduleName);
                File moduleDir = 
                    new File(tmpDir, massagedModuleName);
                J2EEModuleExploder.explodeJar(srcArchive, moduleDir);
            
                // delete the original module file
                srcArchive.delete();
            }
        }

        if (policyChangeListener != null) {
            policyChangeListener.propertyChange(getPolicyChangeEvent(getPlacesFromExpansion(tmpDir)));
        }

        /*
         *Leave the new archive open so the caller can use it directly.
         */
        return appArchive;
    }

    private PropertyChangeEvent getPolicyChangeEvent(final URL[] urls) {
        return new PropertyChangeEvent(this, "places'", null, urls);
    }

    private URL[] getPlacesFromExpansion(final File tmpDir) throws MalformedURLException {
        Collection<URL> places = new ArrayList<URL>();

        for (File f : tmpDir.listFiles(new FileFilter() {

             public boolean accept(File pathname) {
                   return ( pathname.isDirectory() || pathname.getName().endsWith(".jar"));
             }
         })) {
             places.add(f.toURI().toURL());

        }
        return places.toArray(new URL[places.size()]);
    }


    /**
     * Construct the classpaths.  The classpaths constructed here is
     * slightly different from the backend.  It does not process any
     * web module.  The paths included are:
     * 1. all the module root directory (since expansion is needed)
     * 2. all the .jar files found in the archive
     */
    protected List<String> getClassPaths(AbstractArchive archive) {

        List<String> paths = new ArrayList();
        String appRoot = archive.getArchiveUri(); 
        paths.add(appRoot);

        if (appDesc != null) {
            //add all module roots
            Iterator<ModuleDescriptor> bundles = appDesc.getModules();
            while (bundles.hasNext()) {
                ModuleDescriptor bundle = bundles.next();
                String moduleRoot = appRoot + File.separator +
                    FileUtils.makeFriendlyFilename(bundle.getArchiveUri());
                paths.add(moduleRoot);
                
                // Because the app client submodule - like any submodule - is
                // expanded into a directory, the normal manifest processing
                // done for JARs will not be applied to it when this directory is added to
                // the class path of the EJBClassLoader.  So we need to explicitly
                // add the Class-Path elements from the manifest to the class path now.
                File manifestFile = new File(moduleRoot, JarFile.MANIFEST_NAME);
                if ( ! manifestFile.exists()) {
                    continue;
                }
                Manifest mf = null;
                InputStream manifestIS = null;
                try {
                    manifestIS = new FileInputStream(manifestFile);
                    mf = new Manifest(manifestIS);
                    Attributes mainAttrs = mf.getMainAttributes();
                    if (mainAttrs != null) {
                        String classPathString = mainAttrs.getValue(Attributes.Name.CLASS_PATH);
                        if (classPathString != null) {
                            URI appRootURI = new File(appRoot).toURI();
                            String bundleURIString = bundle.getArchiveUri();
                            int lastSlash = bundleURIString.lastIndexOf("/");
                            String parentURIString;
                            if (lastSlash >=0) {
                                parentURIString = bundleURIString.substring(0, lastSlash);
                            } else {
                                parentURIString = "";
                            }
                            String parentDirPath = new File(appRootURI.resolve(parentURIString)).getAbsolutePath();
                            for (String classPathElement : classPathString.split(" ")) {
                                paths.add(parentDirPath + File.separator + classPathElement.replace("/",File.separator));
                            }
                        }
                    }
                } catch (Exception e) {
                    throw new RuntimeException(localStrings.getString("appclient.cannotOpenModuleManifest", bundle.getArchiveUri()), e);
                } finally {
                    if (manifestIS != null) {
                        try {
                            manifestIS.close();
                        } catch (IOException ioe) {
                            throw new RuntimeException(localStrings.getString("appclient.cannotCloseModuleManifest", bundle.getArchiveUri()), ioe);
                        }
                    }
                }
            }
        } else {
            //@@@ read it from the archive
            //shouldn't ever be here though since the appDesc should have been
            //initialized when expand() is called.
        }

        //add all jar files
        for (Enumeration en = archive.entries(); en.hasMoreElements(); ) {
            String entryName = (String) en.nextElement();
            if (entryName.endsWith(".jar")) {
                String entry = appRoot + File.separator + entryName;
                paths.add(entry);
            }
        }

        return paths;
    }

    protected String getAppClientRoot(
        AbstractArchive archive, ApplicationClientDescriptor descriptor) {
        String appRoot = archive.getArchiveUri();
        String moduleUri = descriptor.getModuleDescriptor().getArchiveUri();
        String moduleRoot = appRoot + File.separator +
                    FileUtils.makeFriendlyFilename(moduleUri);
        return moduleRoot;
    }
}
