/*
 * 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.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
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.text.MessageFormat;
import java.util.Arrays;
import java.util.Vector;
import java.util.jar.JarFile;
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";
    
    /** placeholder used in the policy template to substitute dynamically-generated grant clauses */
    private static final String GRANT_CLAUSES_PROPERTY_EXPR = "${grant.clauses}";
    
    /** 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;
    
    /** unpublished command-line argument conveying jwsacc information */
    private static final String JWSACC_ARGUMENT_PREFIX = "-jwsacc";
    
    private static final String JWSACC_EXIT_AFTER_RETURN = "ExitAfterReturn";
    
    /** grant clause template for dynamically populating the policy */
    private static final String GRANT_CLAUSE_TEMPLATE = "grant codeBase \"{0}\" '{'\n" +
	"    permission java.security.AllPermission;\n" + 
        "'}';";
    
    /**
     * request to exit the JVM upon return from the client - should be set (via
     * the -jwsacc command-line argument value) only for 
     * command-line clients; otherwise it can prematurely end the JVM when
     * the GUI and other user work is continuing
     */
    private static boolean exitAfterReturn = false;
    
    /** helper for building the class loader and policy changes */
    private static ClassPathManager classPathManager = null;
    
    /** URLs for downloaded JAR files to be used in the class path */
    private static URL [] downloadedJarURLs;
    
    /** 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) {
        try {
            classPathManager = getClassPathManager();
            args = prepareJWSArgs(args);
            downloadedJarURLs = classPathManager.locateDownloadedJars();
        } catch (Throwable thr) {
            System.err.println("Error locating downloaded jars");
            thr.printStackTrace();
            System.exit(1);
        }
        
        /*
         *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);
        int exitValue = 0;
        try {
            File downloadedAppclientJarFile = findAppClientFileForJWSLaunch(getClass().getClassLoader());

            ClassLoader loader = prepareClassLoader(downloadedAppclientJarFile);

            /*
             *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) {
            exitValue = 1;
            throw new RuntimeException(thr);
        } finally {
            /*
             *If the user has requested, invoke System.exit as soon as the main 
             *method returns.  Do so on the Swing event thread so the ACC
             *main can complete whatever it may be doing.
             */
            if (exitAfterReturn) {
                SwingUtilities.invokeLater(new Runnable() {
                    private int statusValue;
                    public void run() {
                        System.out.printf("Exiting after return from client with status %1$d%n", statusValue);
                        System.exit(statusValue);
                    }
                    
                    public Runnable init(int exitStatus) {
                        statusValue = exitStatus;
                        return this;
                    }
                }.init(exitValue));
            }
        }
    }
    
    /**
     *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) {
        Vector<String> JWSACCArgs = new Vector<String>();
        Vector<String> nonJWSACCArgs = new Vector<String>();
        for (String arg : args) {
            if (arg.startsWith(JWSACC_ARGUMENT_PREFIX)) {
                JWSACCArgs.add(arg.substring(JWSACC_ARGUMENT_PREFIX.length()));
            } else {
                nonJWSACCArgs.add(arg);
            }
        }
        
        processJWSArgs(JWSACCArgs);
        return nonJWSACCArgs.toArray(new String[nonJWSACCArgs.size()]);
    }
    
    /**
     *Interpret the JWSACC arguments (if any) supplied on the command line.
     *@param args the JWSACC arguments
     */
    private static void processJWSArgs(Vector<String> args) {
        for (String arg : args) {
            if (arg.equals(JWSACC_EXIT_AFTER_RETURN)) {
                exitAfterReturn = true;
            }
        }
    }
    
    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);
            
            /*
             *Prepare the grant clauses for the downloaded jars and substitute 
             *those clauses into the policy template.
             */
            StringBuilder grantClauses = new StringBuilder();

            for (URL url : downloadedJarURLs) {
                grantClauses.append(MessageFormat.format(GRANT_CLAUSE_TEMPLATE, url.toExternalForm()));
            }
            
            String substitutedPermissionsTemplate = permissionsTemplate.replace(GRANT_CLAUSES_PROPERTY_EXPR, grantClauses.toString());
            boolean retainTempFiles = Boolean.getBoolean(Main.APPCLIENT_RETAIN_TEMP_FILES_PROPERTYNAME);
            File policyFile = writeTextToTempFile(substitutedPermissionsTemplate, "jwsacc", ".policy", retainTempFiles);

            refreshPolicy(policyFile);
            
        } catch (IOException ioe) {
            throw new RuntimeException("Error loading permissions template", ioe);
        }
    }

    /**
     *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();
            }
        }
    }
     
    /**
     *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.
     *@param downloadedAppclientJarFile the app client jar file
     *@return the class loader
     */
    private static ClassLoader prepareClassLoader(File downloadedAppclientJarFile) throws IOException, URISyntaxException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        ClassLoader ldr = new URLClassLoader(downloadedJarURLs, classPathManager.getParentClassLoader());
        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, MalformedURLException, IllegalAccessException, InvocationTargetException {
        File result = null;
        /*
         *Use the specified class loader to find the resource.
         */
        URL resourceURL = loader.getResource(target);
        if (resourceURL != null) {
            result = classPathManager.findContainingJar(resourceURL);
        }
        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, MalformedURLException, IllegalAccessException, InvocationTargetException {
        /*
         *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;
    }
    
    /**
     *Return the class path manager appropriate to the current version.
     *@return the correct type of ClassPathManager
     */
    public static ClassPathManager getClassPathManager() throws ClassNotFoundException, NoSuchMethodException {
        return ClassPathManager.getClassPathManager();
    }
}
