8001533: java launcher must launch javafx applications
authorksrini
Mon, 19 Nov 2012 19:49:38 -0800
changeset 14518 f4b1adde53b3
parent 14517 e50acb436a8d
child 14519 73d8a1fdacd6
8001533: java launcher must launch javafx applications Reviewed-by: ksrini, mchung, kcr, alanb Contributed-by: david.dehaven@oracle.com
jdk/src/share/bin/java.c
jdk/src/share/classes/sun/launcher/LauncherHelper.java
jdk/src/share/classes/sun/launcher/resources/launcher.properties
jdk/test/tools/launcher/TestHelper.java
--- a/jdk/src/share/bin/java.c	Tue Nov 20 09:26:38 2012 +0000
+++ b/jdk/src/share/bin/java.c	Mon Nov 19 19:49:38 2012 -0800
@@ -105,6 +105,7 @@
                               InvocationFunctions *ifn);
 static jstring NewPlatformString(JNIEnv *env, char *s);
 static jclass LoadMainClass(JNIEnv *env, int mode, char *name);
+static jclass GetApplicationClass(JNIEnv *env);
 
 static void TranslateApplicationArgs(int jargc, const char **jargv, int *pargc, char ***pargv);
 static jboolean AddApplicationOptions(int cpathc, const char **cpathv);
@@ -346,6 +347,7 @@
     JavaVM *vm = 0;
     JNIEnv *env = 0;
     jclass mainClass = NULL;
+    jclass appClass = NULL; // actual application class being launched
     jmethodID mainID;
     jobjectArray mainArgs;
     int ret = 0;
@@ -419,10 +421,28 @@
      *          all environments,
      *     2)   Remove the vestages of maintaining main_class through
      *          the environment (and remove these comments).
+     *
+     * This method also correctly handles launching existing JavaFX
+     * applications that may or may not have a Main-Class manifest entry.
      */
     mainClass = LoadMainClass(env, mode, what);
     CHECK_EXCEPTION_NULL_LEAVE(mainClass);
-    PostJVMInit(env, mainClass, vm);
+    /*
+     * In some cases when launching an application that needs a helper, e.g., a
+     * JavaFX application with no main method, the mainClass will not be the
+     * applications own main class but rather a helper class. To keep things
+     * consistent in the UI we need to track and report the application main class.
+     */
+    appClass = GetApplicationClass(env);
+    NULL_CHECK(appClass);
+    /*
+     * PostJVMInit uses the class name as the application name for GUI purposes,
+     * for example, on OSX this sets the application name in the menu bar for
+     * both SWT and JavaFX. So we'll pass the actual application class here
+     * instead of mainClass as that may be a launcher or helper class instead
+     * of the application class.
+     */
+    PostJVMInit(env, appClass, vm);
     /*
      * The LoadMainClass not only loads the main class, it will also ensure
      * that the main method's signature is correct, therefore further checking
@@ -1215,6 +1235,20 @@
     return (jclass)result;
 }
 
+static jclass
+GetApplicationClass(JNIEnv *env)
+{
+    jmethodID mid;
+    jobject result;
+    jclass cls = GetLauncherHelperClass(env);
+    NULL_CHECK0(cls);
+    NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
+                "getApplicationClass",
+                "()Ljava/lang/Class;"));
+
+    return (*env)->CallStaticObjectMethod(env, cls, mid);
+}
+
 /*
  * For tools, convert command line args thus:
  *   javac -cp foo:foo/"*" -J-ms32m ...
--- a/jdk/src/share/classes/sun/launcher/LauncherHelper.java	Tue Nov 20 09:26:38 2012 +0000
+++ b/jdk/src/share/classes/sun/launcher/LauncherHelper.java	Mon Nov 19 19:49:38 2012 -0800
@@ -69,7 +69,6 @@
 public enum LauncherHelper {
     INSTANCE;
     private static final String MAIN_CLASS = "Main-Class";
-
     private static StringBuilder outBuf = new StringBuilder();
 
     private static final String INDENT = "    ";
@@ -87,6 +86,9 @@
         private static final ResourceBundle RB =
                 ResourceBundle.getBundle(defaultBundleName);
     }
+    private static PrintStream ostream;
+    private static final ClassLoader scloader = ClassLoader.getSystemClassLoader();
+    private static Class<?> appClass; // application class, for GUI/reporting purposes
 
     /*
      * A method called by the launcher to print out the standard settings,
@@ -114,27 +116,27 @@
             long initialHeapSize, long maxHeapSize, long stackSize,
             boolean isServer) {
 
-        PrintStream ostream = (printToStderr) ? System.err : System.out;
+        initOutput(printToStderr);
         String opts[] = optionFlag.split(":");
         String optStr = (opts.length > 1 && opts[1] != null)
                 ? opts[1].trim()
                 : "all";
         switch (optStr) {
             case "vm":
-                printVmSettings(ostream, initialHeapSize, maxHeapSize,
-                        stackSize, isServer);
+                printVmSettings(initialHeapSize, maxHeapSize,
+                                stackSize, isServer);
                 break;
             case "properties":
-                printProperties(ostream);
+                printProperties();
                 break;
             case "locale":
-                printLocale(ostream);
+                printLocale();
                 break;
             default:
-                printVmSettings(ostream, initialHeapSize, maxHeapSize,
-                        stackSize, isServer);
-                printProperties(ostream);
-                printLocale(ostream);
+                printVmSettings(initialHeapSize, maxHeapSize, stackSize,
+                                isServer);
+                printProperties();
+                printLocale();
                 break;
         }
     }
@@ -142,7 +144,7 @@
     /*
      * prints the main vm settings subopt/section
      */
-    private static void printVmSettings(PrintStream ostream,
+    private static void printVmSettings(
             long initialHeapSize, long maxHeapSize,
             long stackSize, boolean isServer) {
 
@@ -172,14 +174,14 @@
     /*
      * prints the properties subopt/section
      */
-    private static void printProperties(PrintStream ostream) {
+    private static void printProperties() {
         Properties p = System.getProperties();
         ostream.println(PROP_SETTINGS);
         List<String> sortedPropertyKeys = new ArrayList<>();
         sortedPropertyKeys.addAll(p.stringPropertyNames());
         Collections.sort(sortedPropertyKeys);
         for (String x : sortedPropertyKeys) {
-            printPropertyValue(ostream, x, p.getProperty(x));
+            printPropertyValue(x, p.getProperty(x));
         }
         ostream.println();
     }
@@ -188,8 +190,7 @@
         return key.endsWith(".dirs") || key.endsWith(".path");
     }
 
-    private static void printPropertyValue(PrintStream ostream,
-            String key, String value) {
+    private static void printPropertyValue(String key, String value) {
         ostream.print(INDENT + key + " = ");
         if (key.equals("line.separator")) {
             for (byte b : value.getBytes()) {
@@ -229,7 +230,7 @@
     /*
      * prints the locale subopt/section
      */
-    private static void printLocale(PrintStream ostream) {
+    private static void printLocale() {
         Locale locale = Locale.getDefault();
         ostream.println(LOCALE_SETTINGS);
         ostream.println(INDENT + "default locale = " +
@@ -238,11 +239,11 @@
                 Locale.getDefault(Category.DISPLAY).getDisplayName());
         ostream.println(INDENT + "default format locale = " +
                 Locale.getDefault(Category.FORMAT).getDisplayName());
-        printLocales(ostream);
+        printLocales();
         ostream.println();
     }
 
-    private static void printLocales(PrintStream ostream) {
+    private static void printLocales() {
         Locale[] tlocales = Locale.getAvailableLocales();
         final int len = tlocales == null ? 0 : tlocales.length;
         if (len < 1 ) {
@@ -370,7 +371,7 @@
      * initHelpSystem must be called before using this method.
      */
     static void printHelpMessage(boolean printToStderr) {
-        PrintStream ostream = (printToStderr) ? System.err : System.out;
+        initOutput(printToStderr);
         outBuf = outBuf.append(getLocalizedMessage("java.launcher.opt.footer",
                 File.pathSeparator));
         ostream.println(outBuf.toString());
@@ -380,7 +381,7 @@
      * Prints the Xusage text to the desired output stream.
      */
     static void printXUsageMessage(boolean printToStderr) {
-        PrintStream ostream =  (printToStderr) ? System.err : System.out;
+        initOutput(printToStderr);
         ostream.println(getLocalizedMessage("java.launcher.X.usage",
                 File.pathSeparator));
         if (System.getProperty("os.name").contains("OS X")) {
@@ -389,36 +390,32 @@
         }
     }
 
-    static String getMainClassFromJar(PrintStream ostream, String jarname) {
-        try {
-            JarFile jarFile = null;
-            try {
-                jarFile = new JarFile(jarname);
-                Manifest manifest = jarFile.getManifest();
-                if (manifest == null) {
-                    abort(ostream, null, "java.launcher.jar.error2", jarname);
-                }
-                Attributes mainAttrs = manifest.getMainAttributes();
-                if (mainAttrs == null) {
-                    abort(ostream, null, "java.launcher.jar.error3", jarname);
-                }
-                String mainValue = mainAttrs.getValue(MAIN_CLASS);
-                if (mainValue == null) {
-                    abort(ostream, null, "java.launcher.jar.error3", jarname);
-                }
-                return mainValue.trim();
-            } finally {
-                if (jarFile != null) {
-                    jarFile.close();
-                }
+    static void initOutput(boolean printToStderr) {
+        ostream =  (printToStderr) ? System.err : System.out;
+    }
+
+    static String getMainClassFromJar(String jarname) {
+        String mainValue = null;
+        try (JarFile jarFile = new JarFile(jarname)) {
+            Manifest manifest = jarFile.getManifest();
+            if (manifest == null) {
+                abort(null, "java.launcher.jar.error2", jarname);
             }
+            Attributes mainAttrs = manifest.getMainAttributes();
+            if (mainAttrs == null) {
+                abort(null, "java.launcher.jar.error3", jarname);
+            }
+            mainValue = mainAttrs.getValue(MAIN_CLASS);
+            if (mainValue == null) {
+                abort(null, "java.launcher.jar.error3", jarname);
+            }
+            return mainValue.trim();
         } catch (IOException ioe) {
-            abort(ostream, ioe, "java.launcher.jar.error1", jarname);
+            abort(ioe, "java.launcher.jar.error1", jarname);
         }
         return null;
     }
 
-
     // From src/share/bin/java.c:
     //   enum LaunchMode { LM_UNKNOWN = 0, LM_CLASS, LM_JAR };
 
@@ -426,7 +423,7 @@
     private static final int LM_CLASS   = 1;
     private static final int LM_JAR     = 2;
 
-    static void abort(PrintStream ostream, Throwable t, String msgKey, Object... args) {
+    static void abort(Throwable t, String msgKey, Object... args) {
         if (msgKey != null) {
             ostream.println(getLocalizedMessage(msgKey, args));
         }
@@ -450,19 +447,22 @@
      *    b. is there a main
      *    c. is the main public
      *    d. is the main static
-     *    c. does the main take a String array for args
-     * 4. and off we go......
+     *    e. does the main take a String array for args
+     * 4. if no main method and if the class extends FX Application, then call
+     *    on FXHelper to determine the main class to launch
+     * 5. and off we go......
      *
-     * @param printToStderr
-     * @param isJar
-     * @param name
-     * @return
+     * @param printToStderr if set, all output will be routed to stderr
+     * @param mode LaunchMode as determined by the arguments passed on the
+     * command line
+     * @param what either the jar file to launch or the main class when using
+     * LM_CLASS mode
+     * @return the application's main class
      */
     public static Class<?> checkAndLoadMain(boolean printToStderr,
                                             int mode,
                                             String what) {
-        final PrintStream ostream = (printToStderr) ? System.err : System.out;
-        final ClassLoader ld = ClassLoader.getSystemClassLoader();
+        initOutput(printToStderr);
         // get the class name
         String cn = null;
         switch (mode) {
@@ -470,44 +470,75 @@
                 cn = what;
                 break;
             case LM_JAR:
-                cn = getMainClassFromJar(ostream, what);
+                cn = getMainClassFromJar(what);
                 break;
             default:
                 // should never happen
                 throw new InternalError("" + mode + ": Unknown launch mode");
         }
         cn = cn.replace('/', '.');
-        Class<?> c = null;
+        Class<?> mainClass = null;
         try {
-            c = ld.loadClass(cn);
-        } catch (ClassNotFoundException cnfe) {
-            abort(ostream, cnfe, "java.launcher.cls.error1", cn);
+            mainClass = scloader.loadClass(cn);
+        } catch (NoClassDefFoundError | ClassNotFoundException cnfe) {
+            abort(cnfe, "java.launcher.cls.error1", cn);
         }
-        getMainMethod(ostream, c);
-        return c;
+        // set to mainClass, FXHelper may return something else
+        appClass = mainClass;
+
+        Method m = getMainMethod(mainClass);
+        if (m != null) {
+            // this will abort if main method has the wrong signature
+            validateMainMethod(m);
+            return mainClass;
+        }
+
+        // Check if FXHelper can launch it using the FX launcher
+        Class<?> fxClass = FXHelper.getFXMainClass(mainClass);
+        if (fxClass != null) {
+            return fxClass;
+        }
+
+        // not an FX application either, abort with an error
+        abort(null, "java.launcher.cls.error4", mainClass.getName(),
+              FXHelper.JAVAFX_APPLICATION_CLASS_NAME);
+        return null; // avoid compiler error...
     }
 
-    static Method getMainMethod(PrintStream ostream, Class<?> clazz) {
-        String classname = clazz.getName();
-        Method method = null;
+    /*
+     * Accessor method called by the launcher after getting the main class via
+     * checkAndLoadMain(). The "application class" is the class that is finally
+     * executed to start the application and in this case is used to report
+     * the correct application name, typically for UI purposes.
+     */
+    public static Class<?> getApplicationClass() {
+        return appClass;
+    }
+
+    // Check for main method or return null if not found
+    static Method getMainMethod(Class<?> clazz) {
         try {
-            method = clazz.getMethod("main", String[].class);
-        } catch (NoSuchMethodException nsme) {
-            abort(ostream, null, "java.launcher.cls.error4", classname);
-        }
+            return clazz.getMethod("main", String[].class);
+        } catch (NoSuchMethodException nsme) {}
+        return null;
+    }
+
+    // Check the signature of main and abort if it's incorrect
+    static void validateMainMethod(Method mainMethod) {
         /*
          * getMethod (above) will choose the correct method, based
          * on its name and parameter type, however, we still have to
          * ensure that the method is static and returns a void.
          */
-        int mod = method.getModifiers();
+        int mod = mainMethod.getModifiers();
         if (!Modifier.isStatic(mod)) {
-            abort(ostream, null, "java.launcher.cls.error2", "static", classname);
+            abort(null, "java.launcher.cls.error2", "static",
+                  mainMethod.getDeclaringClass().getName());
         }
-        if (method.getReturnType() != java.lang.Void.TYPE) {
-            abort(ostream, null, "java.launcher.cls.error3", classname);
+        if (mainMethod.getReturnType() != java.lang.Void.TYPE) {
+            abort(null, "java.launcher.cls.error3",
+                  mainMethod.getDeclaringClass().getName());
         }
-        return method;
     }
 
     private static final String encprop = "sun.jnu.encoding";
@@ -519,7 +550,7 @@
      * previously implemented as a native method in the launcher.
      */
     static String makePlatformString(boolean printToStderr, byte[] inArray) {
-        final PrintStream ostream = (printToStderr) ? System.err : System.out;
+        initOutput(printToStderr);
         if (encoding == null) {
             encoding = System.getProperty(encprop);
             isCharsetSupported = Charset.isSupported(encoding);
@@ -530,7 +561,7 @@
                     : new String(inArray);
             return out;
         } catch (UnsupportedEncodingException uee) {
-            abort(ostream, uee, null);
+            abort(uee, null);
         }
         return null; // keep the compiler happy
     }
@@ -611,5 +642,65 @@
             return "StdArg{" + "arg=" + arg + ", needsExpansion=" + needsExpansion + '}';
         }
     }
+
+    static final class FXHelper {
+        private static final String JAVAFX_APPLICATION_CLASS_NAME =
+                "javafx.application.Application";
+        private static final String JAVAFX_LAUNCHER_CLASS_NAME =
+                "com.sun.javafx.application.LauncherImpl";
+
+        /*
+         * FX application launcher and launch method, so we can launch
+         * applications with no main method.
+         */
+        private static Class<?> fxLauncherClass    = null;
+        private static Method   fxLauncherMethod   = null;
+
+        /*
+         * We can assume that the class does NOT have a main method or it would
+         * have been handled already. We do, however, need to check if the class
+         * extends Application and the launcher is available and abort with an
+         * error if it's not.
+         */
+        private static Class<?> getFXMainClass(Class<?> mainClass) {
+            // Check if mainClass extends Application
+            if (!doesExtendFXApplication(mainClass)) {
+                return null;
+            }
+
+            // Check for the FX launcher classes
+            try {
+                fxLauncherClass = scloader.loadClass(JAVAFX_LAUNCHER_CLASS_NAME);
+                fxLauncherMethod = fxLauncherClass.getMethod("launchApplication",
+                        Class.class, String[].class);
+            } catch (ClassNotFoundException | NoSuchMethodException ex) {
+                abort(ex, "java.launcher.cls.error5", ex);
+            }
+
+            // That's all, return this class so we can launch later
+            return FXHelper.class;
+        }
+
+        /*
+         * Check if the given class is a JavaFX Application class. This is done
+         * in a way that does not cause the Application class to load or throw
+         * ClassNotFoundException if the JavaFX runtime is not available.
+         */
+        private static boolean doesExtendFXApplication(Class<?> mainClass) {
+            for (Class<?> sc = mainClass.getSuperclass(); sc != null;
+                    sc = sc.getSuperclass()) {
+                if (sc.getName().equals(JAVAFX_APPLICATION_CLASS_NAME)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        // preloader ?
+        public static void main(String... args) throws Exception {
+            // launch appClass via fxLauncherMethod
+            fxLauncherMethod.invoke(null, new Object[] {appClass, args});
+        }
+    }
 }
 
--- a/jdk/src/share/classes/sun/launcher/resources/launcher.properties	Tue Nov 20 09:26:38 2012 +0000
+++ b/jdk/src/share/classes/sun/launcher/resources/launcher.properties	Mon Nov 19 19:49:38 2012 -0800
@@ -131,7 +131,10 @@
 \   public static void main(String[] args)
 java.launcher.cls.error4=\
     Error: Main method not found in class {0}, please define the main method as:\n\
-\   public static void main(String[] args)
+\   public static void main(String[] args)\n\
+    or a JavaFX application class must extend {1}
+java.launcher.cls.error5=\
+    Error: JavaFX runtime components are missing, and are required to run this application
 java.launcher.jar.error1=\
     Error: An unexpected error occurred while trying to open file {0}
 java.launcher.jar.error2=manifest not found in {0}
--- a/jdk/test/tools/launcher/TestHelper.java	Tue Nov 20 09:26:38 2012 +0000
+++ b/jdk/test/tools/launcher/TestHelper.java	Mon Nov 19 19:49:38 2012 -0800
@@ -559,6 +559,16 @@
             return false;
         }
 
+        boolean notContains(String str) {
+             for (String x : testOutput) {
+                if (x.contains(str)) {
+                    appendError("string <" + str + "> found");
+                    return false;
+                }
+            }
+            return true;
+        }
+
         boolean matches(String stringToMatch) {
           for (String x : testOutput) {
                 if (x.matches(stringToMatch)) {