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

import com.sun.enterprise.appclient.Main;
import com.sun.enterprise.appclient.jws.Util;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.Policy;
import java.util.Vector;
import javax.swing.SwingUtilities;

/**
 *Alternate main class for ACC, used when launched by Java Web Start.
 *<p>
 *This class assigns security permissions needed by the app server code and
 *by the app client code, then starts the regular app client container.
 *<p>
 *Note that any logic this class executes that requires privileged access
 *must occur either:
 *- from a class in the signed jar containing this class, or
 *- after setPermissions has been invoked.
 *This is because Java Web Start grants elevated permissions only to the classes
 *in the appserv-jwsacc-signed.jar at the beginning.  Only after setPermissions
 *has been invoked can other app server-provided code run with all permissions.
 *
 * @author tjquinn
 */
public class JWSACCMain implements Runnable {
    
//    /** path to a class in one of the app server lib jars downloaded by Java Web Start */
//    private static final String APPSERVER_LIB_CLASS_NAME = "com.sun.enterprise.server.ApplicationServer";
    
    /** name of the permissions template */
    private static final String PERMISSIONS_TEMPLATE_NAME = "jwsclient.policy";
    
    /** property names used in the permissions */
//    private static final String ASLIB_PROPERTY_NAME = "com.sun.aas.jws.lib";
//    private static final String IMQLIB_PROPERTY_NAME = "com.sun.aas.jws.imqlib";

    /** line separator */
    private static final String lineSep = System.getProperty("line.separator");
    
    /** the instance of the acc's main class */
    private static Main accMain = null;
    
    /** the user-specified security policy template to use */
    private static String jwsPolicyTemplateURL = null;
    
    /** make the arguments passed to the constructor available to the main method */
    private String args[];
    
    /** Creates a new instance of JWSMain */
    public JWSACCMain(String[] args) {
        this.args = args;
    }
    
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        args = prepareJWSArgs(args);
        /*
         *Before creating the new instance of the real ACC main, set permissions
         *so ACC and the user's app client can function properly.
         */
        setPermissions();

        /*
         *Make sure that the main ACC class is instantiated and run in the
         *same thread.  Java Web Start may not normally do so.
         */
        JWSACCMain jwsACCMain = new JWSACCMain(args);

        try {
            SwingUtilities.invokeAndWait(jwsACCMain);
            /*
             *Note that the app client is responsible for closing all GUI
             *components or the JVM will never exit.
             */
        } catch (Throwable thr) {
            System.err.println("Error attempting to launch JWSACCMain.main");
            thr.printStackTrace(System.err);
            System.exit(1);
        }
            
    }
    
    public void run() {
//        Main.main(args);
        try {
            File downloadedAppclientJarFile = findAppClientFileForJWSLaunch(getClass().getClassLoader());

            ClassLoader loader = prepareClassLoader();

            /*
             *Set a property that the ACC will retrieve during a JWS launch
             *to locate the app client jar file.
             */
            System.setProperty("com.sun.aas.downloaded.appclient.jar", downloadedAppclientJarFile.getAbsolutePath());

            Thread.currentThread().setContextClassLoader(loader);
            
            /*
             *Use the prepared class loader to load the ACC main method, prepare
             *the arguments to the constructor, and invoke the static main method.
             */
            Method mainMethod = null;
            Class mainClass = Class.forName("com.sun.enterprise.appclient.Main", false /* initialize */, loader);
            mainMethod = mainClass.getMethod(
                    "main", 
                    new Class[] { String[].class } );
            Object params [] = new Object [1];
            params[0] = args;
            mainMethod.invoke(null /* no object -> static method invocation */, params);        
        } catch(Throwable thr) {
            throw new RuntimeException(thr);
        }
    }
    
    /**
     *Process any command line arguments that are targeted for the
     *Java Web Start ACC main program (this class) as opposed to the
     *regular ACC or the client itself.
     *@param args the original command line arguments
     *@return command arguments with any handled by JWS ACC removed
     */
    private static String[] prepareJWSArgs(String[] args) {
        /*
         *For future use to intercept any special-purpose arguments
         *intended for the JWS ACC rather than the command-line ACC or the 
         *user app client.
         */
        return args;
    }
    
    private static void setPermissions() {
        String JWSACCMainClassName = JWSACCMain.class.getName();
        try {
            /*
             *Get the permissions template and write it to a temporary file.
             */
            String permissionsTemplate = Util.loadResource(JWSACCMain.class, PERMISSIONS_TEMPLATE_NAME);

            boolean retainTempFiles = Boolean.getBoolean(Main.APPCLIENT_RETAIN_TEMP_FILES_PROPERTYNAME);
            File policyFile = writeTextToTempFile(permissionsTemplate, "jwsacc", ".policy", retainTempFiles);
            
            /*
             *Find the directory containing the source of the class.  This is the parent
             *of the jar or directory where the class's jar resides.
             */
            String aslibDirURI = deriveURI(JWSACCMainClassName);
            String imqlibDirURI = deriveURI(JWSACCMainClassName);
            
            /*
             *Set the property that is used in the permissions.
             */
            System.setProperty("com.sun.aas.jws.lib",  aslibDirURI);
            System.setProperty("com.sun.aas.jws.imqlib", imqlibDirURI);

            refreshPolicy(policyFile);
            
        } catch (ClassNotFoundException cnfe) {
            /*
             *Error locating the class.
             */
            throw new IllegalStateException("Error locating class " + JWSACCMainClassName, cnfe);
        } catch (URISyntaxException se) {
            throw new RuntimeException("Error locating class" + JWSACCMainClassName, se);
        } catch (IOException ioe) {
            throw new RuntimeException("Error loading permissions template", ioe);
        }
        
        
    }

    /**
     *Loads a resource from a URL.
     *@param urlString the source from which to load the resource
     *@return the content of the resource as a String
     *@throws
     */
    private static String loadURLResource(String urlString) throws MalformedURLException, IOException {
        URL url;
        InputStream is;
        BufferedReader reader = null;
        
        try {
            url = new URL(urlString);
            is = url.openStream();
            reader = new BufferedReader(new InputStreamReader(is));
            StringBuilder content = new StringBuilder();
            String nextLine;
            while ((nextLine = reader.readLine()) != null) {
                content.append(nextLine).append(lineSep);
            }
            return content.toString();
            
        } finally {
            if (reader != null) {
                reader.close();
            }
        }
    }
    
    
    /**
     *Given a class name, derive the URI for the parent of the source of that
     *class.
     *@param the class name to locate
     *@return the URI string for the parent containing the source of that class
     */
    private static String deriveURI(String className) throws ClassNotFoundException, URISyntaxException {
        String result;
        Class cls = JWSACCMain.class.forName(className);
        URL locationURL = locateClass(cls);
        URI locationURI = locationURL.toURI();
        String ssp = locationURI.getSchemeSpecificPart();
        File location = new File(ssp);
        File locationParent = location.getParentFile();
        
        result = locationParent.toURI().toString();

        return result;
    }
           

    /**
     *Locates the first free policy.url.x setting.
     *@return the int value for the first unused policy setting
     */
    public static int firstFreePolicyIndex() {
        int i = 0;
        String propValue;
        do {
            propValue = java.security.Security.getProperty("policy.url." + String.valueOf(++i));
        } while ((propValue != null) && ( ! propValue.equals("")));
        
        return i;
    }
    
    /**
     *Refreshes the current policy object using the contents of the specified file
     *as additional policy.
     *@param policyFile the file containing additional policy 
     */
    public static void refreshPolicy(File policyFile) {
        int idx = firstFreePolicyIndex();
        URI policyFileURI = policyFile.toURI();
        java.security.Security.setProperty("policy.url." + idx, policyFileURI.toASCIIString());
        Policy p = Policy.getPolicy();
        p.refresh();
    }
    
    /**
     *The methods below are duplicates from the com.sun.enterprise.appclient.jws.Util class.
     *At the time this class is running, Java Web Start will not yet permit the Util class to
     *use the elevated permissions.  In fact, this class is in the process of granting
     *those permissions to all app server code.  By including the code here, Java Web Start
     *will permit it to run because this class was loaded from a trusted jar file.
     */

    /**
      *Writes the provided text to a temporary file marked for deletion on exit.
      *@param the content to be written
      *@param prefix for the temp file, conforming to the File.createTempFile requirements
      *@param suffix for the temp file
      *@return File object for the newly-created temp file
      *@throws IOException for any errors writing the temporary file
      *@throws FileNotFoundException if the temp file cannot be opened for any reason
      */
     private static File writeTextToTempFile(String content, String prefix, String suffix, boolean retainTempFiles) throws IOException, FileNotFoundException {
        BufferedWriter wtr = null;
        try {
            File result = File.createTempFile(prefix, suffix);
            if ( ! retainTempFiles) {
                result.deleteOnExit();
            }
            FileOutputStream fos = new FileOutputStream(result);
            wtr = new BufferedWriter(new OutputStreamWriter(fos));
            wtr.write(content);
            wtr.close();
            return result;
        } finally {
            if (wtr != null) {
                wtr.close();
            }
        }
    }
     /**
      *Finds the jar file or directory that contains the specified class and returns its URI.
      *@param the class, the containing jar file or directory of which is of interest
      *@return URL to the containing jar file or directory
      */
     private static URL locateClass(Class target) {
         return target.getProtectionDomain().getCodeSource().getLocation();
     }

     /**
      *Finds a class as a resource using the class's class loader.
      *@param className the class to find
      *@return URL for the class; null if not found
      */
     private static URL locateClass(String className) {
         String resourceName = className.replace(".", "/") + ".class";
         return locateResource(resourceName);
     }
     
     /**
      *Finds a resource using the class's class loader.
      *@param resourceName the class to find
      *@return URL for the resource; null if not found
      */
     private static URL locateResource(String resourceName) {
         URL resourceURL = JWSACCMain.class.getClassLoader().getResource(resourceName);
         return resourceURL;
     }
     
    /**
     *Create the class loader for loading code from the unsigned downloaded
     *app server jars.
     *<p>
     *During a Java Web Start launch the ACC will be run under this class loader.
     *Otherwise the JNLPClassLoader will load any stub classes that are 
     *packaged at the top-level of the generated app client jar file.  (It can
     *see them because it downloaded the gen'd app client jar, and therefore
     *includes the downloaded jar in its class path.  This allows it to see the
     *classes at the top level of the jar but does not automatically let it see
     *classes in the jars nested within the gen'd app client jar.  As a result,
     *the JNLPClassLoader would be the one to try to define the class for a 
     *web services stub, for instance.  But the loader will not be able to find
     *other classes and interfaces needed to completely define the class - 
     *because these are in the jars nested inside the gen'd app client jar.  So
     *the attempt to define the class would fail.
     *@return the class loader
     */
    private static ClassLoader prepareClassLoader() throws IOException, URISyntaxException {
        ClassLoader parent = Thread.currentThread().getContextClassLoader().getParent();
        
        /*
         *For each downloaded unsigned app client jar, get a URL that locates
         *it and add the URL to the class loader's class path.
         *
         *This set of values should be automated on the server side and 
         *communicated via a property setting in the JNLP document so 
         *any changes in the list of downloaded files does not need to be made
         *there and here.  
         */
        String probeClassNames = System.getProperty("com.sun.aas.jar.probe.class.names",
                "com.sun.enterprise.appclient.jws.boot.JWSACCMain," /* appserv-jwsacc */ +
                "com.sun.enterprise.appclient.Main," /* appserv-rt */ +
                "com.sun.jdo.api.persistence.enhancer.ByteCodeEnhancer," /* appserv-cmp */ +
                "com.sun.enterprise.admin.servermgmt.DomainConfig," /* appserv-admin */ +
                "com.sun.enterprise.deployment.client.DeploymentClientUtils," /* appserv-deployment-client */ +
                "javax.ejb.EJB," /* javaee */ +
                "com.sun.appserv.management.ext.logging.LogAnalyzer," /* appserv-ext */ + 
                "com.sun.mail.iap.Argument," /* mail */ +
                "com.sun.activation.registries.LineTokenizer," /* activation */ +
                "com.sun.codemodel.ClassType," /* appserv-ws */ +
                "org.apache.derby.client.ClientDataSourceFactory," /* derbyclient.jar */ +
                "persistence.antlr.ActionElement," /* toplink-essentials */ +
                "org.netbeans.modules.dbschema.ColumnElement," /* dbschema */ +
                "com.sun.jms.spi.xa.JMSXAConnection," /* imqjmsra */ +
                "com.sun.jndi.fscontext.FSContext" /* fscontext */
                );
        
        String [] classNames = probeClassNames.split(",");
        
        /*
         *For each class name, find the jar that contains it by getting a URL
         *to the class and then using that URL to find the jar.
         */
        Vector<URL> classPath = new Vector<URL>();
        for (String className : classNames) {
            URL classURL = locateClass(className);
            File jarFile = findContainingJar(classURL);
            classPath.add(jarFile.toURI().toURL());
        }
        
        /*
         *Create the class loader to use for the ACC's execution.
         */
        URLClassLoader ldr = new URLClassLoader(classPath.toArray(new URL[classPath.size()]), null);
        return ldr;
    }
    
    /*
     *Returns the jar that contains the specified resource.
     *@param target entry name to look for
     *@param loader the class loader to use in finding the resource
     *@return File object for the jar or directory containing the entry
     */
    private static File findContainingJar(String target, ClassLoader loader) throws IllegalArgumentException, URISyntaxException {
        /*
         *Use the specified class loader to find the resource.
         */
        URL resourceURL = loader.getResource(target);
        return findContainingJar(resourceURL);
    }
    
    /*
     *Returns the jar that contains the specified resource.
     *@param resourceURL URL to look for
     *@return File object for the jar or directory containing the entry
     */
    private static File findContainingJar(URL resourceURL) throws IllegalArgumentException, URISyntaxException {
        File result = null;
        if (resourceURL != null) {
            URI uri = resourceURL.toURI();
            String scheme = uri.getScheme();
            String ssp = uri.getSchemeSpecificPart();
            if (scheme.equals("jar")) {
                /*
                 *The scheme-specific part will look like "file:<file-spec>!/<path-to-class>.class"
                 *so we need to isolate the scheme and the <file-spec> part.  
                 *The subscheme (the scheme within the jar) precedes the colon
                 *and the file spec appears after it and before the exclamation point.
                 */
                int colon = ssp.indexOf(':');
                String subscheme = ssp.substring(0, colon);
                int excl = ssp.indexOf('!');
                String containingJarPath = ssp.substring(colon + 1, excl);
                result = new File(containingJarPath);
            } else {
                throw new IllegalArgumentException(resourceURL.toExternalForm());
            }
        }
        return result;
    }
    
    /**
     *Locate the app client jar file during a Java Web Start launch.
     *@param loader the class loader to use in searching for the descriptor entries
     *@return File object for the client jar file
     *@throws IllegalArgumentException if the loader finds neither descriptor
     */
    private File findAppClientFileForJWSLaunch(ClassLoader loader) throws URISyntaxException {
        /*
         *The downloaded jar should contain either META-INF/application.xml or
         *META-INF/application-client.xml.  Look for either one and locate the
         *jar from the URL.
         */
        File containingJar = findContainingJar("META-INF/application.xml", loader);
        if (containingJar == null) {
            containingJar = findContainingJar("META-INF/application-client.xml", loader);
        }
        if (containingJar == null) {
//            needs i18n
//            throw new IllegalArgumentException(localStrings.getString("appclient.JWSnoDownloadedDescr"));
            throw new IllegalArgumentException("Could not locate META-INF/application.xml or META-INF/application-client.xml");
        }
        return containingJar;
    }
}
