/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (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/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

package com.sun.enterprise.appclient;

import com.sun.enterprise.appclient.jws.Util;
import com.sun.enterprise.appclient.jws.boot.JWSACCMain;
import com.sun.enterprise.deployment.annotation.AnnotationProcessorException;
import com.sun.enterprise.deployment.archivist.Archivist;
import com.sun.enterprise.deployment.deploy.shared.AbstractArchive;
import com.sun.enterprise.deployment.*;
import com.sun.enterprise.deployment.util.AnnotationDetector;
import com.sun.enterprise.deployment.util.AppClientPersistenceDependencyAnnotationDetector;
import com.sun.enterprise.loader.EJBClassLoader;
import com.sun.enterprise.loader.InstrumentableClassLoader;
import com.sun.enterprise.util.FileUtil;
import com.sun.enterprise.util.i18n.StringManager;
import com.sun.enterprise.server.PersistenceUnitLoader;
import com.sun.enterprise.server.PersistenceUnitLoaderImpl;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xml.sax.SAXParseException;

import javax.persistence.EntityManagerFactory;

/**
 *Represents information about the app client, regardless of what type of
 *archive (jar or directory) it is stored in or what type of module 
 *(app client or nested within an ear) that archive holds.
 *
 *@author tjquinn
 */

public abstract class AppClientInfo {

    public static final String USER_CODE_IS_SIGNED_PROPERTYNAME = "com.sun.aas.user.code.signed";
    
    private static final String SIGNED_USER_CODE_PERMISSION_TEMPLATE_NAME = "jwsclientSigned.policy";
    
    private static final String UNSIGNED_USER_CODE_PERMISSION_TEMPLATE_NAME = "jwsclientUnsigned.policy";
    
    private static final String CODE_BASE_PLACEHOLDER_NAME = "com.sun.aas.jws.client.codeBase";
    
    /** logger */
    protected Logger _logger;

    /** abstract representation of the storage location */
    private AbstractArchive appClientArchive = null;

    /** original appclient (file or directory) */
    private File appClientFile = null;

    /** abstract archivist able to operate on the module in the location 
      * (specified by archive */
    private Archivist archivist = null;
    
    /** main class name as the user specified it on the command line */
    protected String mainClassFromCommandLine;
    
    /**
     *main class to be used - could come from the command line or from the 
     *manifest of the selected app client archive 
     */
    protected String mainClassNameToRun = null;

    /** class loader - cached */
    private ClassLoader classLoader = null;
    
    /** indicates if the app client has been launched using Java Web Start */
    protected boolean isJWS = false;
    
    /** access to the localizable strings */
    protected StringManager localStrings = 
                        StringManager.getManager(MainWithModuleSupport.class);

    private PersistenceUnitLoader.ApplicationInfo puAppInfo;

    /**
     *Creates a new instance of AppClientInfo.  Always invoked from subclasse
     *because AppClientInfo is abstract.
     *<p>
     *Note that any code instantiationg one of the concrete subclasses MUST
     *invoke completeInit after this super.constructor has returned.
     *
     *@param isJWS indicates if ACC has been launched using Java Web Start
     *@param logger the logger to use for messages
     *@param archive the AbstractArchive for the app client module or 
     *               directory being launched
     *@param archivist the Archivist corresponding to the type of module 
     *                 in the archive
     *@param mainClassFromCommandLine the main class name specified on the 
     *       command line (null if not specified)
     */
    public AppClientInfo(boolean isJWS, Logger logger, File appClientFile, 
                         Archivist archivist, String mainClassFromCommandLine) {
        this.isJWS = isJWS;
        _logger = logger;
        this.appClientFile = appClientFile;
        this.archivist = archivist;
        this.mainClassFromCommandLine = mainClassFromCommandLine;
    }

    /**
     *Finishes initialization work.
     *<p>
     *The calling logic that instantiates this object must invoke completeInit
     *after instantiation but before using the object.
     *@throws IOException for errors opening the expanded archive
     *@throws SAXParseException for errors parsing the descriptors in a newly-opened archive
     *@throws ClassNotFoundException if the main class requested cannot be located in the archive
     *@throws URISyntaxException if preparing URIs for the class loader fails
     *
     */
    protected void completeInit() 
        throws IOException, SAXParseException, ClassNotFoundException, 
               URISyntaxException, AnnotationProcessorException, Exception {

        //expand if needed. initialize the appClientArchive
        appClientArchive = expand(appClientFile);

        //Create the class loader to be used for persistence unit checking, 
        //validation, and running the app client.
        classLoader = createClassLoader(appClientArchive);

        //Populate the deployment descriptor without validation.
        //Note that validation is done only after the persistence handling
        //has instructed the classloader created above.
        RootDeploymentDescriptor descriptor = 
            populateDescriptor(appClientArchive, archivist, classLoader);
        
         //If the selected app client depends on at least one persistence unit
         //then handle the P.U. before proceeding.
        if (appClientDependsOnPersistenceUnit(getAppClient())) {
            //@@@check to see if the descriptor is metadata-complet=true
            //if not, we would have loaded classes into the classloader
            //during annotation processing.  we need to hault and ask
            //the user to deploy the application.
            //if (!getAppClient().isFullFlag()) {
            //    throw new RuntimeException("Please deploy your application");
            //}
            handlePersistenceUnitDependency();
        }
        
         //Now that the persistence handling has run and instrumented the class
         //loader - if it had to - it's ok to validate.
        archivist.validate(classLoader);
        
        fixupWSDLEntries();
        
        if (isJWS) {
            grantRequestedPermissionsToUserCode();
        }
    }
    
    /**
     *Returns the app client descriptor to be run.
     *@return the descriptor for the selected app client
     */
    protected ApplicationClientDescriptor getAppClient() {
        return getAppClient(archivist);
    }

    protected ClassLoader getClassLoader() {
        return classLoader;
    }

    protected File createTmpArchiveDir(File forArchive) 
        throws IOException {
        /*
         *Create a temporary file (cannot create temp directories directly),
         *use the unique file name as a directory name.
         */
        String name = forArchive.getName();
        
        File tmpDir = File.createTempFile("acc-" + name, "");
        tmpDir.delete();

        tmpDir.mkdirs();

        /*
         *We will try to delete the directory when we finish with it, but this
         *is a back-up.
         */
        if (!_keepExplodedDir) {
            tmpDir.deleteOnExit();
        }

        return tmpDir;
    }

    /**
     *Closes the instance of AppClientInfo, deleting any temporary directory
     *created and closing the archive.
     *@throws IOException in case of error closing the archive
     */
    protected void close() throws IOException {
        try {
            if (puAppInfo != null) {
                new PersistenceUnitLoaderImpl().unload(puAppInfo);
                puAppInfo = null;
            }
            if (appClientArchive != null) {
                appClientArchive.close();
            }
            if (classLoader != null && 
                    classLoader instanceof EJBClassLoader) {
                ((EJBClassLoader) classLoader).done();
            }
        } finally {
            if (deleteAppClientDir()) {
                if (appClientArchive != null) {
                    deleteFile(new File(appClientArchive.getArchiveUri()));
                }
            }
            appClientArchive = null;
        }
    }

    protected boolean deleteAppClientDir() {
        return !_keepExplodedDir;
    }
    
    
    /**
     *Processes persistence unit handling for the ACC.
     */
    private void handlePersistenceUnitDependency() 
            throws URISyntaxException, MalformedURLException {
        this.puAppInfo = new ApplicationInfoImpl(this);
        new PersistenceUnitLoaderImpl().load(puAppInfo);
    }

    /**
     * implementation of
     * {@link com.sun.enterprise.server.PersistenceUnitLoader.ApplicationInfo}.
     */
    private static class ApplicationInfoImpl
            implements PersistenceUnitLoader.ApplicationInfo {

        private AppClientInfo outer; // the outer object we are associated with

        private ApplicationClientDescriptor appClient;

        public ApplicationInfoImpl(AppClientInfo outer) {
            this.outer = outer;
            appClient = outer.getAppClient();
        }

        public Application getApplication() {
            Application application = appClient.getApplication();
            if (application == null) {
                application = Application.createApplication(
                        appClient.getModuleID(),
                        appClient.getModuleDescriptor());
            }
            return application;
        }

        public InstrumentableClassLoader getClassLoader() {
            return (InstrumentableClassLoader) outer.getClassLoader();
        }

        public String getApplicationLocation() {
            return outer.appClientArchive.getArchiveUri();
        }

        /**
         * @return list of PU that are actually referenced by the
         *         appclient.
         */
        public Collection<? extends PersistenceUnitDescriptor>
                getReferencedPUs() {
            return appClient.findReferencedPUs();
        }

        /**
         * @return list of EMFs that have been loaded for this appclient.
         */
        public Collection<? extends EntityManagerFactory> getEntityManagerFactories() {
            Collection<EntityManagerFactory> emfs =
                    new HashSet<EntityManagerFactory>();

            if (appClient.getApplication() != null) {
                emfs.addAll(appClient.getApplication()
                        .getEntityManagerFactories());
            }
            emfs.addAll(appClient.getEntityManagerFactories());
            return emfs;
        }
    } // end of class ApplicationInfoImpl

    private ClassLoader createClassLoader(AbstractArchive archive)
        throws IOException {
        List<String> paths = getClassPaths(archive);
        ClassLoader parent = Thread.currentThread().getContextClassLoader();
        EJBClassLoader loader = new EJBClassLoader(parent);

        final int LIST_SZ = paths.size();
        for (int i=0; i<LIST_SZ; i++) {
            String path = paths.get(i);
            loader.appendURL(new File(path));
        }
        
        if (_logger.isLoggable(Level.FINE)) {
            for (int i = 0; i < paths.size(); i++) {
                _logger.fine("Added path to classloader ==> " + paths.get(i));
            }
        }

        return loader;
    }

    private void deleteFile(File f) {
        if (f.isDirectory()) {
            for (File subFile : f.listFiles()) {
                deleteFile(subFile);
            }
        }
        f.delete();
    }

    /**
     *Reports whether the selected app client depends on a persistence unit
     *or not.
     *@returns true if the app client depends on a persistence unit
     */
    private boolean appClientDependsOnPersistenceUnit(
        ApplicationClientDescriptor acDescr) 
            throws MalformedURLException, ClassNotFoundException, 
                   IOException, URISyntaxException {
            /*
             *If the descriptor contains at least one reference to an entity
             *manager then it definitely depends on a persistence unit.  
             */
            return descriptorContainsPURefcs(acDescr) 
                    || mainClassContainsPURefcAnnotations(acDescr);
    }
    
    /**
     *Reports whether the app client's descriptor shows a dependence on a
     *persistence unit.
     *@param descr the descriptor for the app client in question
     *@returns true if the descriptor shows such a dependency
     */
    private  boolean descriptorContainsPURefcs(
        ApplicationClientDescriptor descr) {
        return ! descr.getEntityManagerFactoryReferenceDescriptors().isEmpty();
    }
    
    /**
     *Reports whether the main class in the archive contains annotations that 
     *refer to persistence units.
     *@return boolean if the main class contains annotations that refer to a pers. unit
     */
    private boolean mainClassContainsPURefcAnnotations(
        ApplicationClientDescriptor acDescr) 
            throws MalformedURLException, ClassNotFoundException, 
                   IOException, URISyntaxException {
        AppClientPersistenceDependencyAnnotationDetector annoDetector = 
                    new AppClientPersistenceDependencyAnnotationDetector();

        //e.g. FROM a.b.Foo or Foo TO a/b/Foo.class or Foo.class
        String mainClassEntryName = 
                acDescr.getMainClassName().replace('.', '/') + ".class";

        return classContainsAnnotation
                (mainClassEntryName, annoDetector, appClientArchive, acDescr);
    }

    /**
     *Adjusts the web services WSDL entries corresponding to where they
     *actually reside.
     */
    private void fixupWSDLEntries() 
        throws URISyntaxException, MalformedURLException, IOException, 
               AnnotationProcessorException {
        ApplicationClientDescriptor ac = getAppClient();
        URI uri = (new File(getAppClientRoot(appClientArchive, ac))).toURI();
        File moduleFile = new File(uri);
        for (Iterator itr = ac.getServiceReferenceDescriptors().iterator();
                    itr.hasNext();) {
            ServiceReferenceDescriptor serviceRef = 
                    (ServiceReferenceDescriptor) itr.next();
            if (serviceRef.getWsdlFileUri()!=null) {
                // In case WebServiceRef does not specify wsdlLocation, we get
                // wsdlLocation from @WebClient in wsimport generated source; 
                // If wsimport was given a local WSDL file, then WsdlURI will
                // be an absolute path - in that case it should not be prefixed
                // with modileFileDir
                URI wsdlURI = new URI(serviceRef.getWsdlFileUri());
                File wsdlFile = new File(serviceRef.getWsdlFileUri());
                if(wsdlFile.isAbsolute()) {
                    serviceRef.setWsdlFileUrl(wsdlFile.toURI().toURL());
                } else {
                    // This is the case where WsdlFileUri is a relative path
                    // (hence relative to the root of this module or wsimport
                    // was executed with WSDL in HTTP URL form
                    serviceRef.setWsdlFileUrl(FileUtil.getEntryAsUrl(
                        moduleFile, serviceRef.getWsdlFileUri()));
                }
            }
        }
    }

    private RootDeploymentDescriptor populateDescriptor(
            AbstractArchive archive, Archivist theArchivist, ClassLoader loader) 
        throws IOException, SAXParseException, Exception {

        //@@@ Optimize it later.
        //Here the application.xml is read twice for NestedAppClientInfo.
        //Once already in expand() method.

        theArchivist.setAnnotationProcessingRequested(true);

        //@@@ setting of the classloader would trigger annotation processing 
        //for appclients that have only partial deployment descriptors or no
        //descriptors at all. 
        //Note that the annotation processing is bypassed if the descriptors 
        //are meta-complete=true", which will be the case for anything that is 
        //generated by the backend, i.e. if the appclient being executed here 
        //is a generated jar produced by the appserver, obtained by deploying
        //the original application client and retrieve.
        theArchivist.setClassLoader(loader);

        //open with Archivist./pen(AbstractArchive) to also ensure the
        //validation is not called
        //return archivist.open(archive);
        RootDeploymentDescriptor d = null;
        try {
            d = theArchivist.open(archive);
        } catch (Exception ex) { 
            close(); //make sure there is no junk tmp director left
            throw ex;
        }
        
        //depend on the type of the appclient, additional work needs
        //to be done.
        messageDescriptor(d, theArchivist, archive);

        theArchivist.setDescriptor(d);
        return d;
    }

    /**
     *Granting the appropriate level of permissions to user code, emulating
     *the Java Web Start behavior as required in the JNLP spec.
     *<p>
     *Classes from the user's app client jar are loaded using the EJBClassLoader
     *rather than the Java Web Start class loader.  As a result, Java Web Start
     *cannot grant the appropriate level of permissions to the user code since
     *it is the JWS class loader that does that.  So we need to grant the user
     *code the appropriate level of permissions, based on whether the user's
     *app client jar was signed or not.
     *@param retainTempFiles indicates whether to keep the generated policy file
     */
    private void grantRequestedPermissionsToUserCode() throws IOException, URISyntaxException {
        /*
         *Create a temporary file containing permissions grants.  We will use
         *this temp file to refresh the Policy objects's settings.  The temp
         *file will contain one segment for each element in the class path
         *for the user code (from the expanded generated app client jar). 
         *The permissions granted will be the same for all class path elements,
         *and the specific settings will either be the sandbox permissions
         *as described in the JNLP spec or full permissions.
         */
        boolean userJarIsSigned = Boolean.getBoolean(USER_CODE_IS_SIGNED_PROPERTYNAME);
        
        boolean retainTempFiles = Boolean.getBoolean(MainWithModuleSupport.APPCLIENT_RETAIN_TEMP_FILES_PROPERTYNAME);
        
        /*
         *Use a template to construct each part of the policy file, choosing the
         *template based on whether the user code is signed or not.
         */
        String templateName = (userJarIsSigned ? SIGNED_USER_CODE_PERMISSION_TEMPLATE_NAME : UNSIGNED_USER_CODE_PERMISSION_TEMPLATE_NAME);
        String template = Util.loadResource(JWSACCMain.class, templateName);
        
        /*
         *Create the temporary policy file to write to.
         */
        File policyFile = File.createTempFile("accjws-user", ".policy");
        if ( ! retainTempFiles) {
            policyFile.deleteOnExit();
        }
        
        PrintStream ps = new PrintStream(policyFile);
        
        Properties p = new Properties();
        
        /*
         *Use the list of class paths already set up on the EJBClassLoader.
         *Then for each element in the class path, write a part of the policy 
         *file granting the privs specified in the selected template to the code path.
         */
        EJBClassLoader loader = (EJBClassLoader) getClassLoader();
        for (URL classPathElement : loader.getURLs()) {
            /*
             *Convert the URL into a proper codebase expression suitable for
             *a grant clause in the policy file.
             */
            String codeBase = Util.URLtoCodeBase(classPathElement);
            if (codeBase != null) {
                p.setProperty(CODE_BASE_PLACEHOLDER_NAME, codeBase);
                String policyPart = Util.replaceTokens(template, p);

                ps.println(policyPart);
            }
        }
        
        /*
         *Close the temp file and use it to refresh the current Policy object.
         */
        ps.close();
        
        JWSACCMain.refreshPolicy(policyFile);
        
        if ( ! retainTempFiles) {
            policyFile.delete();
        }
    }
    
    /////////////////////////////////////////////////////////////////
    //  The following protected methods are overridden by at least //
    //  one of the sub classes.                                    //
    /////////////////////////////////////////////////////////////////

    /**
     *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 archive for the expanded directory archive
     *@exception IOException in case of errors during the expansion
     */
    protected abstract AbstractArchive expand(File file) 
        throws IOException, Exception;

    protected ApplicationClientDescriptor getAppClient(
        Archivist archivist) {
        return ApplicationClientDescriptor.class.cast(
                    archivist.getDescriptor());
    }

    protected String getAppClientRoot(
        AbstractArchive archive, ApplicationClientDescriptor descriptor) {
        return archive.getArchiveUri();
    }                                        

    protected void messageDescriptor(RootDeploymentDescriptor d, 
        Archivist archivist, AbstractArchive archive) 
            throws IOException, AnnotationProcessorException {
        //default behavor: no op
    }

    protected List<String> getClassPaths(AbstractArchive archive) {
        List<String> paths = new ArrayList();
        paths.add(archive.getArchiveUri());
        return paths;
    }

    /**
     *Returns the main class that should be executed.
     *@return the name of the main class to execute when the client starts
     */
    protected String getMainClassNameToRun(ApplicationClientDescriptor acDescr) {
         if (mainClassNameToRun == null) {
             if (mainClassFromCommandLine != null) {
                 mainClassNameToRun = mainClassFromCommandLine;
                 _logger.fine("Main class is " + mainClassNameToRun + " from command line");
             } else {
                 /*
                  *Find out which class to execute from the descriptor.
                  */
                 mainClassNameToRun = getAppClient().getMainClassName();
                 _logger.fine("Main class is " + mainClassNameToRun + " from descriptor");
             }
         }
         return mainClassNameToRun;
    }

    protected boolean classContainsAnnotation(
            String entry, AnnotationDetector detector, 
            AbstractArchive archive, ApplicationClientDescriptor descriptor) 
            throws FileNotFoundException, IOException {
        String acRoot = getAppClientRoot(archive, descriptor);
        String entryLocation = acRoot + File.separator + entry;
        File entryFile = new File(entryLocation);
        return detector.containsAnnotation(entryFile);
    }

    public String toString() {
        String lineSep = System.getProperty("line.separator");
        StringBuilder result = new StringBuilder();
        result.append(this.getClass().getName() + ": " + lineSep);
        result.append("  isJWS: " + isJWS);
        result.append("  archive file: " + appClientFile.getAbsolutePath() + lineSep);
        result.append("  archive type: " + appClientArchive.getClass().getName() + lineSep);
        result.append("  archivist type: " + archivist.getClass().getName() + lineSep);
        result.append("  main class to be run: " + mainClassNameToRun + lineSep);
        result.append("  temporary archive directory: " + appClientArchive.getArchiveUri() + lineSep);
        result.append("  class loader type: " + classLoader.getClass().getName() + lineSep);
       
        return result.toString();
    }

    //for debug purpose
    protected static final boolean _keepExplodedDir = 
            Boolean.getBoolean("appclient.keep.exploded.dir");
}
