diff options
author | Sven Gothel <[email protected]> | 2014-01-30 10:39:16 +0100 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2014-01-30 10:39:16 +0100 |
commit | 64e7dcc21339ae56841f10131a4f8a462454dec4 (patch) | |
tree | a62d2fbafc6fbe412d2d5ad599cd992bdf97ddbe /netx/jogamp/plugin | |
parent | 98c9d6e1ea22db18913b531b8056fbdc5465eb18 (diff) |
Experimental Applet without AWT (Applet3)
DISCLAIMER:
- Only new Applet3 applets are supported under X11 for now
- AWT Applet are disabled
- Namespace com.jogamp.* and jogamp.* is only chosen
to indicate new AWT-less code
- Applet3 code path does not invoke any AWT function
- JNLP code path still utilizes AWT/Swing (UIs, ..)
TODO:
- Refactor AWT dependencies properly via UI interfaces ?
- Decide whether we shall merge netx and plugin namespace ?
IMHO the right thing to do, jumping hoops due to separation.
- Add support for Windows, OSX, Wayland, ..
Applet3:
- New AWT-less Applet3 interfaces are:
- com.jogamp.plugin.applet.Applet3
- User implements
- com.jogamp.plugin.applet.Applet3Context
- Plugin implements
- com.jogamp.plugin.ui.NativeWindowUpstream
- Plugin window, aka browser parent of Applet3
- com.jogamp.plugin.ui.NativeWindowDownstream
- Applet3 user window
- User interfaces are exported as:
- plugin3-public.jar
- plugin3-public-src.zip
Diffstat (limited to 'netx/jogamp/plugin')
-rw-r--r-- | netx/jogamp/plugin/jnlp/App3Launcher.java | 882 | ||||
-rw-r--r-- | netx/jogamp/plugin/jnlp/NetxApplet3Panel.java | 230 | ||||
-rw-r--r-- | netx/jogamp/plugin/jnlp/runtime/Applet3Environment.java | 273 | ||||
-rw-r--r-- | netx/jogamp/plugin/jnlp/runtime/Applet3Instance.java | 134 | ||||
-rw-r--r-- | netx/jogamp/plugin/jnlp/runtime/JNLP3SecurityManager.java | 388 |
5 files changed, 1907 insertions, 0 deletions
diff --git a/netx/jogamp/plugin/jnlp/App3Launcher.java b/netx/jogamp/plugin/jnlp/App3Launcher.java new file mode 100644 index 0000000..1d0b9c6 --- /dev/null +++ b/netx/jogamp/plugin/jnlp/App3Launcher.java @@ -0,0 +1,882 @@ +// Copyright (C) 2001-2003 Jon A. Maxwell (JAM) +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +package jogamp.plugin.jnlp; + +import static net.sourceforge.jnlp.runtime.Translator.R; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import jogamp.applet.App3Context; +import jogamp.plugin.jnlp.runtime.Applet3Instance; + +import com.jogamp.plugin.applet.Applet3; + +import net.sourceforge.jnlp.AppletDesc; +import net.sourceforge.jnlp.ApplicationDesc; +import net.sourceforge.jnlp.JARDesc; +import net.sourceforge.jnlp.JNLPFile; +import net.sourceforge.jnlp.LaunchException; +import net.sourceforge.jnlp.LaunchHandler; +import net.sourceforge.jnlp.ParserSettings; +import net.sourceforge.jnlp.PluginBridge; +import net.sourceforge.jnlp.PropertyDesc; +import net.sourceforge.jnlp.ResourcesDesc; +import net.sourceforge.jnlp.StreamEater; +import net.sourceforge.jnlp.util.JarFile; +import net.sourceforge.jnlp.cache.CacheUtil; +import net.sourceforge.jnlp.cache.UpdatePolicy; +import net.sourceforge.jnlp.runtime.ApplicationInstance; +import net.sourceforge.jnlp.runtime.JNLPRuntime; +import net.sourceforge.jnlp.runtime.JNLPClassLoader; +import net.sourceforge.jnlp.services.InstanceExistsException; +import net.sourceforge.jnlp.services.ServiceUtil; +import net.sourceforge.jnlp.splashscreen.SplashUtils; +import net.sourceforge.jnlp.util.logging.OutputController; + +/** + * Launches JNLPFiles either in the foreground or background.<p> + * + * An optional LaunchHandler can be specified that is notified of + * warning and error condition while launching and that indicates + * whether a launch may proceed after a warning has occurred. If + * specified, the LaunchHandler is notified regardless of whether + * the file is launched in the foreground or background.<p> + * + * @author <a href="mailto:[email protected]">Jon A. Maxwell (JAM)</a> - initial author + * @version $Revision: 1.22 $ + */ +public class App3Launcher { + + // defines class Launcher.BgRunner, Launcher.TgThread + + /** shared thread group */ + /*package*/static final ThreadGroup mainGroup = new ThreadGroup(R("LAllThreadGroup")); + + public static ThreadGroup getMainGroup() { return mainGroup; } + + /** the handler */ + private LaunchHandler handler = null; + + /** the update policy */ + private UpdatePolicy updatePolicy = JNLPRuntime.getDefaultUpdatePolicy(); + + /** whether to create an AppContext (if possible) */ + private boolean context = true; + + /** If the application should call JNLPRuntime.exit on fatal errors */ + private boolean exitOnFailure = true; + + private ParserSettings parserSettings = new ParserSettings(); + + private Map<String, String[]> extra = null; + + /** + * Create a launcher with the runtime's default update policy + * and launch handler. + */ + public App3Launcher() { + this(null, null); + + if (handler == null) { + handler = JNLPRuntime.getDefaultLaunchHandler(); + } + } + + /** + * Create a launcher with the runtime's default update policy + * and launch handler. + * + * @param exitOnFailure Exit if there is an error (usually default, but false when being used from the plugin) + */ + public App3Launcher(boolean exitOnFailure) { + this(null, null); + + if (handler == null) { + handler = JNLPRuntime.getDefaultLaunchHandler(); + } + + this.exitOnFailure = exitOnFailure; + } + + /** + * Create a launcher with the specified handler and the + * runtime's default update policy. + * + * @param handler the handler to use or null for no handler. + */ + public App3Launcher(LaunchHandler handler) { + this(handler, null); + } + + /** + * Create a launcher with an optional handler using the + * specified update policy and launch handler. + * + * @param handler the handler to use or null for no handler. + * @param policy the update policy to use or null for default policy. + */ + public App3Launcher(LaunchHandler handler, UpdatePolicy policy) { + if (policy == null) + policy = JNLPRuntime.getDefaultUpdatePolicy(); + + this.handler = handler; + this.updatePolicy = policy; + + } + + /** + * Sets the update policy used by launched applications. + */ + public void setUpdatePolicy(UpdatePolicy policy) { + if (policy == null) { + throw new IllegalArgumentException(R("LNullUpdatePolicy")); + } + + this.updatePolicy = policy; + } + + /** + * Returns the update policy used when launching applications. + */ + public UpdatePolicy getUpdatePolicy() { + return updatePolicy; + } + + /** + * Sets whether to launch the application in a new AppContext + * (a separate event queue, look and feel, etc). If the + * sun.awt.SunToolkit class is not present then this method + * has no effect. The default value is true. + */ + public void setCreateAppContext(boolean context) { + this.context = context; + } + + /** + * Returns whether applications are launched in their own + * AppContext. + */ + public boolean isCreateAppContext() { + return this.context; + } + + /** + * Set the parser settings to use when the Launcher initiates parsing of + * a JNLP file. + * @param settings + */ + public void setParserSettings(ParserSettings settings) { + parserSettings = settings; + } + + /** + * Set a map to use when trying to extract extra information, including + * arguments, properties and parameters, to be merged into the main JNLP + * @param input a map containing extra information to add to the main JNLP. + * the values for keys "arguments", "parameters", and "properties" are + * used. + */ + public void setInformationToMerge(Map<String, String[]> input) { + this.extra = input; + } + + /** + * Launches a JNLP file by calling the launch method for the + * appropriate file type. The application will be started in + * a new window. + * + * @param file the JNLP file to launch + * @return the application instance + * @throws LaunchException if an error occurred while launching (also sent to handler) + */ + public ApplicationInstance launch(JNLPFile file) throws LaunchException { + TgThread tg; + + mergeExtraInformation(file, extra); + + JNLPRuntime.markNetxRunning(); + + //First checks whether offline-allowed tag is specified inside the jnlp + //file. + if (!file.getInformation().isOfflineAllowed()) { + try { + //Checks the offline/online status of the system. + //If system is offline do not launch. + InetAddress.getByName(file.getSourceLocation().getHost()); + + } catch (UnknownHostException ue) { + OutputController.getLogger().log(OutputController.Level.ERROR_ALL, "File cannot be launched because offline-allowed tag not specified and system currently offline."); + return null; + } catch (Exception e) { + OutputController.getLogger().log(e); + } + } + + if (file instanceof PluginBridge) { + tg = new TgThread(file, true); + } + else { + tg = new TgThread(file); + } + + tg.start(); + + try { + tg.join(); + } catch (InterruptedException ex) { + //By default, null is thrown here, and the message dialog is shown. + throw launchWarning(new LaunchException(file, ex, R("LSMinor"), R("LCSystem"), R("LThreadInterrupted"), R("LThreadInterruptedInfo"))); + } + + if (tg.getException() != null) { + throw tg.getException(); + } // passed to handler when first created + + if (handler != null) { + // FIXME handler.launchCompleted(tg.getApplication()); + } + + return tg.getApplication(); + } + + + /** + * Launches a JNLP file by calling the launch method for the + * appropriate file type. + * + * @param location the URL of the JNLP file to launch + * location to get the pristine version + * @throws LaunchException if there was an exception + * @return the application instance + */ + public ApplicationInstance launch(URL location) throws LaunchException { + return launch(fromUrl(location)); + } + + /** + * Merges extra information into the jnlp file + * + * @param file the JNLPFile + * @param extra extra information to merge into the JNLP file + * @throws LaunchException if an exception occurs while extracting + * extra information + */ + private void mergeExtraInformation(JNLPFile file, Map<String, String[]> extra) throws LaunchException { + if (extra == null) { + return; + } + + String[] properties = extra.get("properties"); + if (properties != null) { + addProperties(file, properties); + } + + String[] arguments = extra.get("arguments"); + if (arguments != null && file.isApplication()) { + addArguments(file, arguments); + } + + String[] parameters = extra.get("parameters"); + if (parameters != null && file.isApplet()) { + addParameters(file, parameters); + } + } + + /** + * Add the properties to the JNLP file. + * @throws LaunchException if an exception occurs while extracting + * extra information + */ + private void addProperties(JNLPFile file, String[] props) throws LaunchException { + ResourcesDesc resources = file.getResources(); + + for (int i = 0; i < props.length; i++) { + // allows empty property, not sure about validity of that. + int equals = props[i].indexOf("="); + if (equals == -1) { + throw launchError(new LaunchException(R("BBadProp", props[i]))); + } + + String key = props[i].substring(0, equals); + String value = props[i].substring(equals + 1, props[i].length()); + + resources.addResource(new PropertyDesc(key, value)); + } + } + + /** + * Add the params to the JNLP file; only call if file is + * actually an applet file. + * @throws LaunchException if an exception occurs while extracting + * extra information + */ + private void addParameters(JNLPFile file, String[] params) throws LaunchException { + AppletDesc applet = file.getApplet(); + + for (int i = 0; i < params.length; i++) { + // allows empty param, not sure about validity of that. + int equals = params[i].indexOf("="); + if (equals == -1) { + throw launchError(new LaunchException(R("BBadParam", params[i]))); + } + + String name = params[i].substring(0, equals); + String value = params[i].substring(equals + 1, params[i].length()); + + applet.addParameter(name, value); + } + } + + /** + * Add the arguments to the JNLP file; only call if file is + * actually an application (not installer). + */ + private void addArguments(JNLPFile file, String[] args) { + ApplicationDesc app = file.getApplication(); + + for (int i = 0; i < args.length; i++) { + app.addArgument(args[i]); + } + } + + + /** + * Launches the JNLP file in a new JVM instance. The launched + * application's output is sent to the system out and it's + * standard input channel is closed. + * + * @param vmArgs the arguments to pass to the new JVM. Can be empty but + * must not be null. + * @param file the JNLP file to launch + * @param javawsArgs the arguments to pass to the javaws command. Can be + * an empty list but must not be null. + * @throws LaunchException if there was an exception + */ + public void launchExternal(List<String> vmArgs, JNLPFile file, List<String> javawsArgs) throws LaunchException { + List<String> updatedArgs = new LinkedList<String>(javawsArgs); + + if (file.getFileLocation() != null) { + updatedArgs.add(file.getFileLocation().toString()); + } + else if (file.getSourceLocation() != null) { + updatedArgs.add(file.getFileLocation().toString()); + } + else { + launchError(new LaunchException(file, null, R("LSFatal"), R("LCExternalLaunch"), R("LNullLocation"), R("LNullLocationInfo"))); + } + + launchExternal(vmArgs, updatedArgs); + + } + + /** + * Launches the JNLP file in a new JVM instance. The launched + * application's output is sent to the system out and it's + * standard input channel is closed. + * + * @param url the URL of the JNLP file to launch + * @throws LaunchException if there was an exception + */ + public void launchExternal(URL url) throws LaunchException { + List<String> javawsArgs = new LinkedList<String>(); + javawsArgs.add(url.toString()); + launchExternal(new LinkedList<String>(), javawsArgs); + } + + /** + * Launches the JNLP file at the specified location in a new JVM + * instance. The launched application's output is sent to the + * system out and it's standard input channel is closed. + * @param vmArgs the arguments to pass to the jvm + * @param javawsArgs the arguments to pass to javaws (aka Netx) + * @throws LaunchException if there was an exception + */ + public void launchExternal(List<String> vmArgs, List<String> javawsArgs) throws LaunchException { + try { + + List<String> commands = new LinkedList<String>(); + + // this property is set by the javaws launcher to point to the javaws binary + String pathToWebstartBinary = System.getProperty("icedtea-web.bin.location"); + commands.add(pathToWebstartBinary); + // use -Jargument format to pass arguments to the JVM through the launcher + for (String arg : vmArgs) { + commands.add("-J" + arg); + } + commands.addAll(javawsArgs); + + String[] command = commands.toArray(new String[] {}); + + Process p = Runtime.getRuntime().exec(command); + new StreamEater(p.getErrorStream()).start(); + new StreamEater(p.getInputStream()).start(); + p.getOutputStream().close(); + + } catch (NullPointerException ex) { + throw launchError(new LaunchException(null, null, R("LSFatal"), R("LCExternalLaunch"), R("LNetxJarMissing"), R("LNetxJarMissingInfo"))); + } catch (Exception ex) { + throw launchError(new LaunchException(null, ex, R("LSFatal"), R("LCExternalLaunch"), R("LCouldNotLaunch"), R("LCouldNotLaunchInfo"))); + } + } + + /** + * Returns the JNLPFile for the URL, with error handling. + */ + private JNLPFile fromUrl(URL location) throws LaunchException { + try { + JNLPFile file = new JNLPFile(location, parserSettings); + + boolean isLocal = false; + boolean haveHref = false; + if ("file".equalsIgnoreCase(location.getProtocol()) && new File(location.getFile()).exists()) { + isLocal = true; + } + if (file.getSourceLocation() != null) { + haveHref = true; + } + + if (isLocal && haveHref) { + file = new JNLPFile(file.getSourceLocation(), parserSettings); + } + return file; + } catch (Exception ex) { + if (ex instanceof LaunchException) { + throw (LaunchException) ex; // already sent to handler when first thrown + } else { + // IO and Parse + throw launchError(new LaunchException(null, ex, R("LSFatal"), R("LCReadError"), R("LCantRead"), R("LCantReadInfo"))); + } + } + } + + /** + * Launches a JNLP application. This method should be called + * from a thread in the application's thread group. + */ + protected ApplicationInstance launchApplication(JNLPFile file) throws LaunchException { + if (!file.isApplication()) { + throw launchError(new LaunchException(file, null, R("LSFatal"), R("LCClient"), R("LNotApplication"), R("LNotApplicationInfo"))); + } + + try { + + try { + ServiceUtil.checkExistingSingleInstance(file); + } catch (InstanceExistsException e) { + OutputController.getLogger().log("Single instance application is already running."); + return null; + } + + if (JNLPRuntime.getForksAllowed() && file.needsNewVM()) { + List<String> netxArguments = new LinkedList<String>(); + netxArguments.add("-Xnofork"); + netxArguments.addAll(JNLPRuntime.getInitialArguments()); + launchExternal(file.getNewVMArgs(), netxArguments); + return null; + } + + handler.launchInitialized(file); + + ApplicationInstance app = createApplication(file); + app.initialize(); + + String mainName = file.getApplication().getMainClass(); + + // When the application-desc field is empty, we should take a + // look at the main jar for the main class. + if (mainName == null) { + JARDesc mainJarDesc = file.getResources().getMainJAR(); + File f = CacheUtil.getCacheFile(mainJarDesc.getLocation(), null); + if (f != null) { + final JarFile mainJar = new JarFile(f); + try { + mainName = mainJar.getManifest().getMainAttributes().getValue("Main-Class"); + } finally { + mainJar.close(); + } + } + } + + if (mainName == null) { + throw launchError(new LaunchException(file, null, + R("LSFatal"), R("LCClient"), R("LCantDetermineMainClass"), + R("LCantDetermineMainClassInfo"))); + } + + Class<?> mainClass = app.getClassLoader().loadClass(mainName); + + Method main = mainClass.getMethod("main", new Class<?>[] { String[].class }); + String args[] = file.getApplication().getArguments(); + + setContextClassLoaderForAllThreads(app.getThreadGroup(), app.getClassLoader()); + + handler.launchStarting(app); + + main.setAccessible(true); + + OutputController.getLogger().log("Invoking main() with args: " + Arrays.toString(args)); + main.invoke(null, new Object[] { args }); + + return app; + } catch (LaunchException lex) { + throw launchError(lex); + } catch (Exception ex) { + throw launchError(new LaunchException(file, ex, R("LSFatal"), R("LCLaunching"), R("LCouldNotLaunch"), R("LCouldNotLaunchInfo"))); + } + } + + /** + * Set the classloader as the context classloader for all threads in + * the given threadgroup. This is required to make some applications + * work. For example, an application that provides a custom Swing LnF + * may ask the swing thread to load resources from their JNLP, which + * would only work if the Swing thread knows about the JNLPClassLoader. + * + * @param tg The threadgroup for which the context classloader should be set + * @param classLoader the classloader to set as the context classloader + */ + private void setContextClassLoaderForAllThreads(ThreadGroup tg, ClassLoader classLoader) { + + /* be prepared for change in thread size */ + int threadCountGuess = tg.activeCount(); + Thread[] threads; + do { + threadCountGuess = threadCountGuess * 2; + threads = new Thread[threadCountGuess]; + tg.enumerate(threads, true); + } while (threads[threadCountGuess - 1] != null); + + for (Thread thread : threads) { + if (thread != null) { + OutputController.getLogger().log(OutputController.Level.ERROR_DEBUG, "Setting " + classLoader + " as the classloader for thread " + thread.getName()); + thread.setContextClassLoader(classLoader); + } + } + + } + + /** + * Launches a JNLP applet. This method should be called from a + * thread in the application's thread group.<p> + * + * The enableCodeBase parameter adds the applet's codebase to + * the locations searched for resources and classes. This can + * slow down the applet loading but allows browser-style applets + * that don't use JAR files exclusively to be run from a applet + * JNLP file. If the applet JNLP file does not specify any + * resources then the code base will be enabled regardless of + * the specified value.<p> + * + * @param file the JNLP file + * @param enableCodeBase whether to add the codebase URL to the classloader + */ + protected ApplicationInstance launchApplet(JNLPFile file, boolean enableCodeBase) throws LaunchException { + if (!file.isApplet()) { + throw launchError(new LaunchException(file, null, R("LSFatal"), R("LCClient"), R("LNotApplet"), R("LNotAppletInfo"))); + } + + if (handler != null) { + handler.launchInitialized(file); + } + + Applet3Instance applet = null; + try { + ServiceUtil.checkExistingSingleInstance(file); + applet = createApplet(file, enableCodeBase); + applet.initialize(); + applet.getAppletEnvironment().startApplet(); // this should be a direct call to applet instance + return applet; + } catch (InstanceExistsException ieex) { + OutputController.getLogger().log("Single instance applet is already running."); + throw launchError(new LaunchException(file, ieex, R("LSFatal"), R("LCLaunching"), R("LCouldNotLaunch"), R("LSingleInstanceExists")), applet); + } catch (LaunchException lex) { + throw launchError(lex, applet); + } catch (Exception ex) { + throw launchError(new LaunchException(file, ex, R("LSFatal"), R("LCLaunching"), R("LCouldNotLaunch"), R("LCouldNotLaunchInfo")), applet); + }finally{ + if (handler != null) { + handler.launchStarting(applet); + } + } + } + + /** + * Gets an ApplicationInstance, but does not launch the applet. + */ + protected ApplicationInstance getApplet(JNLPFile file, boolean enableCodeBase) throws LaunchException { + if (!file.isApplet()) { + throw launchError(new LaunchException(file, null, R("LSFatal"), R("LCClient"), R("LNotApplet"), R("LNotAppletInfo"))); + } + Applet3Instance applet = null; + try { + ServiceUtil.checkExistingSingleInstance(file); + applet = createApplet(file, enableCodeBase); + applet.initialize(); + return applet; + + } catch (InstanceExistsException ieex) { + OutputController.getLogger().log("Single instance applet is already running."); + throw launchError(new LaunchException(file, ieex, R("LSFatal"), R("LCLaunching"), R("LCouldNotLaunch"), R("LSingleInstanceExists")), applet); + } catch (LaunchException lex) { + throw launchError(lex, applet); + } catch (Exception ex) { + throw launchError(new LaunchException(file, ex, R("LSFatal"), R("LCLaunching"), R("LCouldNotLaunch"), R("LCouldNotLaunchInfo")), applet); + } + } + + /** + * Launches a JNLP installer. This method should be called from + * a thread in the application's thread group. + */ + protected ApplicationInstance launchInstaller(JNLPFile file) throws LaunchException { + // TODO Check for an existing single instance once implemented. + // ServiceUtil.checkExistingSingleInstance(file); + throw launchError(new LaunchException(file, null, R("LSFatal"), R("LCNotSupported"), R("LNoInstallers"), R("LNoInstallersInfo"))); + } + + /** + * Create an AppletInstance. + * + * @param enableCodeBase whether to add the code base URL to the classloader + */ + //FIXME - when multiple applets are on one page, this method is visited simultaneously + //and then appelts creates in little bit strange manner. This issue is visible with + //randomly showing/notshowing spalshscreens. + //See also PluginAppletViewer.framePanel + protected Applet3Instance createApplet(JNLPFile file, boolean enableCodeBase) throws LaunchException { + Applet3Instance appletInstance = null; + try { + JNLPClassLoader loader = JNLPClassLoader.getInstance(file, updatePolicy); + + if (enableCodeBase) { + loader.enableCodeBase(); + } else if (file.getResources().getJARs().length == 0) { + throw new ClassNotFoundException("Can't do a codebase look up and there are no jars. Failing sooner rather than later"); + } + + ThreadGroup group = Thread.currentThread().getThreadGroup(); + + // appletInstance is needed by ServiceManager when looking up + // services. This could potentially be done in applet constructor + // so initialize appletInstance before creating applet. + appletInstance = new Applet3Instance(file, group, loader, null); + + loader.setApplication(appletInstance); + + // Initialize applet now that ServiceManager has access to its + // appletInstance. + String appletName = file.getApplet().getMainClass(); + Class<?> appletClass = loader.loadClass(appletName); + Applet3 applet = (Applet3) appletClass.newInstance(); + // Finish setting up appletInstance. + appletInstance.setApplet(applet); + appletInstance.getAppletEnvironment().setApplet(applet); + + setContextClassLoaderForAllThreads(appletInstance.getThreadGroup(), appletInstance.getClassLoader()); + + return appletInstance; + } catch (Exception ex) { + throw launchError(new LaunchException(file, ex, R("LSFatal"), R("LCInit"), R("LInitApplet"), R("LInitAppletInfo")), appletInstance); + } + } + + /** + * Creates an Applet object from a JNLPFile. This is mainly to be used with + * gcjwebplugin. + * @param file the PluginBridge to be used. + * @param enableCodeBase whether to add the code base URL to the classloader. + */ + protected Applet3 createAppletObject(JNLPFile file, boolean enableCodeBase) throws LaunchException { + try { + JNLPClassLoader loader = JNLPClassLoader.getInstance(file, updatePolicy); + + if (enableCodeBase) { + loader.enableCodeBase(); + } else if (file.getResources().getJARs().length == 0) { + throw new ClassNotFoundException("Can't do a codebase look up and there are no jars. Failing sooner rather than later"); + } + + String appletName = file.getApplet().getMainClass(); + Class<?> appletClass = loader.loadClass(appletName); + Applet3 applet = (Applet3) appletClass.newInstance(); + + return applet; + } catch (Exception ex) { + throw launchError(new LaunchException(file, ex, R("LSFatal"), R("LCInit"), R("LInitApplet"), R("LInitAppletInfo"))); + } + } + + /** + * Creates an Application. + */ + protected ApplicationInstance createApplication(JNLPFile file) throws LaunchException { + try { + JNLPClassLoader loader = JNLPClassLoader.getInstance(file, updatePolicy); + ThreadGroup group = Thread.currentThread().getThreadGroup(); + + ApplicationInstance app = new ApplicationInstance(file, group, loader, App3Context.getAppContext()); + loader.setApplication(app); + + return app; + } catch (Exception ex) { + throw new LaunchException(file, ex, R("LSFatal"), R("LCInit"), R("LInitApplication"), R("LInitApplicationInfo")); + } + } + + /** + * Create a thread group for the JNLP file. + * + * Note: if the JNLPFile is an applet (ie it is a subclass of PluginBridge) + * then this method simply returns the existing ThreadGroup. The applet + * ThreadGroup has to be created at an earlier point in the applet code. + */ + protected ThreadGroup createThreadGroup(JNLPFile file) { + final ThreadGroup tg; + + if (file instanceof PluginBridge) { + tg = Thread.currentThread().getThreadGroup(); + } else { + tg = new ThreadGroup(mainGroup, file.getTitle()); + } + + return tg; + } + + /** + * Send n launch error to the handler, if set, and also to the + * caller. + */ + private LaunchException launchError(LaunchException ex) { + return launchError(ex, null); + } + + private LaunchException launchError(LaunchException ex, Applet3Instance applet) { + if (applet != null) { + SplashUtils.showErrorCaught(ex, applet); + } + if (handler != null) { + handler.launchError(ex); + } + + return ex; + } + + /** + * Send a launch error to the handler, if set, and to the + * caller only if the handler indicated that the launch should + * continue despite the warning. + * + * @return an exception to throw if the launch should be aborted, or null otherwise + */ + private LaunchException launchWarning(LaunchException ex) { + if (handler != null) { + if (!handler.launchWarning(ex)) + // no need to destroy the app b/c it hasn't started + return ex; + } // chose to abort + + return null; // chose to continue, or no handler + } + + /** + * This runnable is used to call the appropriate launch method + * for the application, applet, or installer in its thread group. + */ + private class TgThread extends Thread { // ThreadGroupThread + private JNLPFile file; + private ApplicationInstance application; + private LaunchException exception; + private boolean isPlugin = false; + + TgThread(JNLPFile file) { + super(createThreadGroup(file), file.getTitle()); + + this.file = file; + } + + TgThread(JNLPFile file, boolean isPlugin) { + super(createThreadGroup(file), file.getTitle()); + this.file = file; + this.isPlugin = isPlugin; + } + + @Override + public void run() { + try { + // Do not create new AppContext if we're using NetX and icedteaplugin. + // The plugin needs an AppContext too, but it has to be created earlier. + /** FIXME: Non plugin context + if (context && !isPlugin) { + SunToolkit.createNewAppContext(); + } + doPerApplicationAppContextHacks(); + */ + + if (isPlugin) { + // Do not display download indicators if we're using gcjwebplugin. + JNLPRuntime.setDefaultDownloadIndicator(null); + application = getApplet(file, ((PluginBridge)file).codeBaseLookup()); + } else { + if (file.isApplication()) { + application = launchApplication(file); + } + else if (file.isApplet()) { + application = launchApplet(file, true); + } // enable applet code base + else if (file.isInstaller()) { + application = launchInstaller(file); + } + else { + throw launchError(new LaunchException(file, null, + R("LSFatal"), R("LCClient"), R("LNotLaunchable"), + R("LNotLaunchableInfo"))); + } + } + } catch (LaunchException ex) { + OutputController.getLogger().log(OutputController.Level.ERROR_ALL, ex); + exception = ex; + // Exit if we can't launch the application. + if (exitOnFailure) { + JNLPRuntime.exit(1); + } + } catch (Throwable ex) { + OutputController.getLogger().log(OutputController.Level.ERROR_ALL, ex); + throw new RuntimeException(ex); + } + } + + public LaunchException getException() { + return exception; + } + + public ApplicationInstance getApplication() { + return application; + } + + }; + + + +} diff --git a/netx/jogamp/plugin/jnlp/NetxApplet3Panel.java b/netx/jogamp/plugin/jnlp/NetxApplet3Panel.java new file mode 100644 index 0000000..875dc5a --- /dev/null +++ b/netx/jogamp/plugin/jnlp/NetxApplet3Panel.java @@ -0,0 +1,230 @@ +/* + * Copyright 2012 Red Hat, Inc. + * This file is part of IcedTea, http://icedtea.classpath.org + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package jogamp.plugin.jnlp; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import jogamp.applet.App3Context; +import jogamp.applet.Applet3Panel; +import jogamp.plugin.jnlp.runtime.Applet3Instance; +import net.sourceforge.jnlp.Launcher; +import net.sourceforge.jnlp.PluginBridge; +import net.sourceforge.jnlp.PluginParameters; +import net.sourceforge.jnlp.runtime.JNLPRuntime; +import net.sourceforge.jnlp.splashscreen.SplashController; +import net.sourceforge.jnlp.splashscreen.SplashPanel; +import net.sourceforge.jnlp.splashscreen.SplashUtils; +import net.sourceforge.jnlp.util.logging.OutputController; + +import com.jogamp.plugin.applet.Applet3Context; + +/** + * This panel calls into netx to run an applet, and pipes the display + * into a panel from the icedtea-web browser plugin. + * + * @author Francis Kung <[email protected]> + */ +public class NetxApplet3Panel extends Applet3Panel implements SplashController { + private final PluginParameters pluginParameters; + private PluginBridge bridge = null; + private Applet3Instance appInst = null; + private SplashController splashController; + /** The plugin .. */ + private Applet3Context upstreamContext = null; + private volatile boolean initialized; + + // We use this so that we can create exactly one thread group + // for all panels with the same uKey. + private static final Map<String, ThreadGroup> uKeyToTG = + new HashMap<String, ThreadGroup>(); + private static final Object TGMapMutex = new Object(); + + // This map is actually a set (unfortunately there is no ConcurrentSet + // in java.util.concurrent). If KEY is in this map, then we know that + // an app context has been created for the panel that has uKey.equals(KEY), + // so we avoid creating it a second time for panels with the same uKey. + // Because it's a set, only the keys matter. However, we can't insert + // null values in because if we did, we couldn't use null checks to see + // if a key was absent before a putIfAbsent. + private static final ConcurrentMap<String, Boolean> appContextCreated = + new ConcurrentHashMap<String, Boolean>(); + + public NetxApplet3Panel(long nativeWindowHandle, int width, int height, URL documentURL, PluginParameters params) { + super(nativeWindowHandle, width, height, documentURL, params.getUnderlyingHashtable()); + + this.pluginParameters = params; + this.initialized = false; + + String uniqueKey = params.getUniqueKey(getCodeBase()); + synchronized(TGMapMutex) { + if (!uKeyToTG.containsKey(uniqueKey)) { + ThreadGroup tg = new ThreadGroup(Launcher.getMainGroup(), this.documentURL.toString()); + uKeyToTG.put(uniqueKey, tg); + } + } + } + + @Override + protected void showAppletException(Throwable t) { + /* + * Log any exceptions thrown while loading, initializing, starting, + * and stopping the applet. + */ + OutputController.getLogger().log(OutputController.Level.MESSAGE_ALL, t); //new logger + super.showAppletException(t); + } + + //Overriding to use Netx classloader. You might need to relax visibility + //in sun.applet.AppletPanel for runLoader(). + @Override + protected void runLoader() { + + try { + bridge = new PluginBridge(baseURL, + getDocumentBase(), + getJarFiles(), + getCode(), + getWidth(), + getHeight(), + pluginParameters); + + doInit = true; + dispatchAppletEvent(APPLET_LOADING, null); + status = APPLET_LOAD; + + App3Launcher l = new App3Launcher(false); + + // May throw LaunchException: + appInst = (Applet3Instance) l.launch(bridge); + applet = appInst.getApplet(); + + if (applet != null) { + // Stick it in the frame + showAppletStatus("loaded"); + } + } catch (Exception e) { + status = APPLET_ERROR; + OutputController.getLogger().log(OutputController.Level.ERROR_ALL, e); + replaceSplash(SplashUtils.getErrorSplashScreen(getWidth(), getHeight(), e)); + } finally { + // PR1157: This needs to occur even in the case of an exception + // so that the applet's event listeners are signaled. + // Once PluginAppletViewer.AppletEventListener is signaled PluginAppletViewer can properly stop waiting + // in PluginAppletViewer.waitForAppletInit + this.initialized = true; + dispatchAppletEvent(APPLET_LOADING_COMPLETED, null); + } + } + + /** + * Creates a new Thread (in a new applet-specific ThreadGroup) for running + * the applet + */ + // Reminder: Relax visibility in sun.applet.AppletPanel + @Override + public synchronized void createAppletThread() { + // initialize JNLPRuntime in the main threadgroup + synchronized (JNLPRuntime.initMutex) { + //The custom NetX Policy and SecurityManager are set here. + if (!JNLPRuntime.isInitialized()) { + OutputController.getLogger().log("initializing JNLPRuntime..."); + + JNLPRuntime.initialize(false); + } else { + OutputController.getLogger().log("JNLPRuntime already initialized"); + } + } + + handler = new Thread(getThreadGroup(), this, "NetxPanelThread@" + this.documentURL); + handler.start(); + } + + public final void setAppletContext(Applet3Context ctx) { + upstreamContext = ctx; + } + + @Override + protected final Applet3Context getAppletContext() { + if( null != upstreamContext ) { + return upstreamContext; + } else { + return appInst.getAppletEnvironment(); + } + } + + @Override + public ClassLoader getAppletClassLoader() { + return appInst.getClassLoader(); + } + + public boolean isInitialized() { + return initialized; + } + + public ThreadGroup getThreadGroup() { + synchronized(TGMapMutex) { + return uKeyToTG.get(pluginParameters.getUniqueKey(getCodeBase())); + } + } + + public void createNewAppContext() { + if (Thread.currentThread().getThreadGroup() != getThreadGroup()) { + throw new RuntimeException("createNewAppContext called from the wrong thread."); + } + // only create a new context if one hasn't already been created for the + // applets with this unique key. + if (null == appContextCreated.putIfAbsent(pluginParameters.getUniqueKey(getCodeBase()), Boolean.TRUE)) { + App3Context.createAppContext(); // FIXME: Really ? + // SunToolkit.createNewAppContext(); + } + } + + public void setAppletViewerFrame(SplashController framePanel) { + splashController=framePanel; + } + + @Override + public void removeSplash() { + splashController.removeSplash(); + } + + @Override + public void replaceSplash(SplashPanel r) { + splashController.replaceSplash(r); + } + + @Override + public int getSplashWidth() { + return splashController.getSplashWidth(); + } + + @Override + public int getSplashHeigth() { + return splashController.getSplashHeigth(); + } + +} diff --git a/netx/jogamp/plugin/jnlp/runtime/Applet3Environment.java b/netx/jogamp/plugin/jnlp/runtime/Applet3Environment.java new file mode 100644 index 0000000..66df33a --- /dev/null +++ b/netx/jogamp/plugin/jnlp/runtime/Applet3Environment.java @@ -0,0 +1,273 @@ +// Copyright (C) 2001-2003 Jon A. Maxwell (JAM) +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +package jogamp.plugin.jnlp.runtime; + +import java.io.InputStream; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; + +import net.sourceforge.jnlp.JNLPFile; +import net.sourceforge.jnlp.splashscreen.SplashController; +import net.sourceforge.jnlp.util.logging.OutputController; + +import com.jogamp.plugin.applet.Applet3; +import com.jogamp.plugin.applet.Applet3Context; + +/** + * The applet environment including stub, context, and frame. The + * default environment puts the applet in a non-resiable frame; + * this can be changed by obtaining the frame and setting it + * resizable. + * + * @author <a href="mailto:[email protected]">Jon A. Maxwell (JAM)</a> - initial author + * @version $Revision: 1.12 $ + */ +public class Applet3Environment implements Applet3Context { + + /** the JNLP file */ + private JNLPFile file; + + /** the applet */ + private Applet3 applet; + + /** the parameters */ + private Map<String, String> parameters; + + /** whether the applet has been started / displayed */ + private boolean appletStarted = false; + + /** whether the applet has been destroyed */ + private boolean destroyed = false; + + /** + * Create a new applet environment for the applet specified by + * the JNLP file. + */ + public Applet3Environment(JNLPFile file, final Applet3Instance appletInstance) { + this.file = file; + this.applet = appletInstance.getApplet(); + + parameters = file.getApplet().getParameters(); + } + + /** + * Checks whether the applet has been destroyed, and throws an + * IllegalStateException if the applet has been destroyed of. + * + * @throws IllegalStateException + */ + private void checkDestroyed() { + if (destroyed) { + throw new IllegalStateException("Illegal applet stub/context access: applet destroyed."); + } + } + + /** + * Disposes the applet's resources and disables the applet + * environment from further use; after calling this method the + * applet stub and context methods throw IllegalStateExceptions. + */ + public void destroy() { + destroyed = true; + } + + /** + * FIXME: Currently null + */ + public SplashController getSplashControler() { + return null; + } + + /** + * Initialize, start, and show the applet. + */ + public void startApplet() { + checkDestroyed(); + + if (appletStarted) { + return; + } + + appletStarted = true; + + try { + // This is only needed if the applet is in its own frame. + applet.init(Applet3Environment.this); + applet.start(); + } catch (Exception ex) { + OutputController.getLogger().log(ex); + + // should also kill the applet? + } + } + + // applet context methods + + /** + * Returns the applet if the applet's name is specified, + * otherwise return null. + */ + @Override + public Applet3Context getAppletContext(String name) { + checkDestroyed(); + + if (name != null && name.equals(file.getApplet().getName())) { + return this; + } else { + return null; + } + } + + /** + * Set the applet of this environment; can only be called once. + */ + public void setApplet(Applet3 applet) { + if (this.applet != null) { + OutputController.getLogger().log(new IllegalStateException("Applet can only be set once.")); + return; + } + this.applet = applet; + } + + @Override + public Enumeration<Applet3Context> getAllAppletContexts() { + checkDestroyed(); + + return Collections.enumeration(Arrays.asList(new Applet3Context[] { this})); + } + + /** + * Not implemented yet. + */ + @Override + public void showDocument(java.net.URL uRL) { + checkDestroyed(); + + } + + /** + * Not implemented yet. + */ + @Override + public void showDocument(java.net.URL uRL, java.lang.String str) { + checkDestroyed(); + + } + + /** + * Not implemented yet. + */ + @Override + public void showStatus(java.lang.String str) { + checkDestroyed(); + + } + + /** + * Required for JRE1.4, but not implemented yet. + */ + @Override + public void setStream(String key, InputStream stream) { + checkDestroyed(); + + } + + /** + * Required for JRE1.4, but not implemented yet. + */ + @Override + public InputStream getStream(String key) { + checkDestroyed(); + + return null; + } + + /** + * Required for JRE1.4, but not implemented yet. + */ + @Override + public Iterator<String> getStreamKeys() { + checkDestroyed(); + + return null; + } + + // stub methods + + @Override + public void resize(int width, int height) { + appletResize(width, height); + } + public void appletResize(int width, int height) { + checkDestroyed(); + // FIXME: ?? + } + + @Override + public Applet3 getApplet() { + checkDestroyed(); + + return applet; + } + + @Override + public URL getCodeBase() { + checkDestroyed(); + + return file.getCodeBase(); + } + + @Override + public URL getDocumentBase() { + checkDestroyed(); + + return file.getApplet().getDocumentBase(); + } + + // FIXME: Sun's applet code forces all parameters to lower case. + // Does Netx's JNLP code do the same, so we can remove the first lookup? + @Override + public String getParameter(String name) { + checkDestroyed(); + + String s = parameters.get(name); + if (s != null) { + return s; + } + + return parameters.get(name.toLowerCase()); + } + + @Override + public boolean isActive() { + checkDestroyed(); + + // it won't be started or stopped, so if it can call it's running + return true; + } + + @Override + public String getAppletName() { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/netx/jogamp/plugin/jnlp/runtime/Applet3Instance.java b/netx/jogamp/plugin/jnlp/runtime/Applet3Instance.java new file mode 100644 index 0000000..7293936 --- /dev/null +++ b/netx/jogamp/plugin/jnlp/runtime/Applet3Instance.java @@ -0,0 +1,134 @@ +// Copyright (C) 2001-2003 Jon A. Maxwell (JAM) +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +package jogamp.plugin.jnlp.runtime; + +import jogamp.applet.App3Context; + +import com.jogamp.plugin.applet.Applet3; + +import net.sourceforge.jnlp.*; +import net.sourceforge.jnlp.runtime.ApplicationInstance; +import net.sourceforge.jnlp.util.logging.OutputController; + +/** + * Represents a launched application instance created from a JNLP + * file. This class does not control the operation of the applet, + * use the AppletEnvironment class to start and stop the applet. + * + * @author <a href="mailto:[email protected]">Jon A. Maxwell (JAM)</a> - initial author + * @version $Revision: 1.9 $ + */ +public class Applet3Instance extends ApplicationInstance { + + /** whether the applet's stop and destroy methods have been called */ + private boolean appletStopped = false; + + /** the applet */ + private Applet3 applet; + + /** the applet environment */ + private Applet3Environment environment; + + /** + * Create a New Task based on the Specified URL + */ + public Applet3Instance(JNLPFile file, ThreadGroup group, ClassLoader loader, Applet3 applet) { + super(file, group, loader, App3Context.getAppContext()); + + this.applet = applet; + + this.environment = new Applet3Environment(file, this); + } + + /** + * Set the applet of this launched application; can only be called once. + */ + public void setApplet(Applet3 applet) { + if (this.applet != null) { + OutputController.getLogger().log(new IllegalStateException("Applet can only be set once.")); + return; + } + this.applet = applet; + } + + /** + * Sets whether the applet is resizable or not. Applets default + * to being not resizable. + */ + public void setResizable(boolean resizable) { + /** FIXME + Container c = environment.getAppletFrame(); + if (c instanceof Frame) + ((Frame) c).setResizable(resizable); */ + } + + /** + * Returns whether the applet is resizable. + */ + public boolean isResizable() { + /** FIXME + Container c = environment.getAppletFrame(); + if (c instanceof Frame) + return ((Frame) c).isResizable(); + + return false; */ + return false; + } + + /** + * Returns the application title. + */ + public String getTitle() { + return getJNLPFile().getApplet().getName(); + } + + /** + * Returns the applet environment. + */ + public Applet3Environment getAppletEnvironment() { + return environment; + } + + /** + * Returns the applet. + */ + public Applet3 getApplet() { + return applet; + } + + /** + * Stop the application and destroy its resources. + */ + public void destroy() { + if (appletStopped) + return; + + appletStopped = true; + + try { + applet.stop(); + applet.destroy(); + } catch (Exception ex) { + OutputController.getLogger().log(ex); + } + + environment.destroy(); + + super.destroy(); + } + +} diff --git a/netx/jogamp/plugin/jnlp/runtime/JNLP3SecurityManager.java b/netx/jogamp/plugin/jnlp/runtime/JNLP3SecurityManager.java new file mode 100644 index 0000000..0804bdc --- /dev/null +++ b/netx/jogamp/plugin/jnlp/runtime/JNLP3SecurityManager.java @@ -0,0 +1,388 @@ +// Copyright (C) 2001-2003 Jon A. Maxwell (JAM) +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +package jogamp.plugin.jnlp.runtime; + +import static net.sourceforge.jnlp.runtime.Translator.R; + +import java.security.AccessControlException; +import java.security.Permission; + +import jogamp.applet.App3Context; +import net.sourceforge.jnlp.runtime.ApplicationInstance; +import net.sourceforge.jnlp.runtime.JNLPClassLoader; +import net.sourceforge.jnlp.runtime.JNLPRuntime; +import net.sourceforge.jnlp.util.logging.OutputController; +import net.sourceforge.jnlp.util.WeakList; + +/** + * Security manager for JNLP environment. This security manager + * cannot be replaced as it always denies attempts to replace the + * security manager or policy.<p> + * + * The JNLP security manager tracks windows created by an + * application, allowing those windows to be disposed when the + * application exits but the JVM does not. If security is not + * enabled then the first application to call System.exit will + * halt the JVM.<p> + * + * @author <a href="mailto:[email protected]">Jon A. Maxwell (JAM)</a> - initial author + * @version $Revision: 1.17 $ + */ +public class JNLP3SecurityManager extends SecurityManager { + + // todo: some apps like JDiskReport can close the VM even when + // an exit class is set - fix! + + // todo: create an event dispatch thread for each application, + // so that the context classloader doesn't have to be switched + // to the foreground application (the currently the approach + // since some apps need their classloader as event dispatch + // thread's context classloader). + + // todo: use a custom Permission object to identify the current + // application in an AccessControlContext by setting a side + // effect in its implies method. Use a custom + // AllPermissions-like permission to do this for apps granted + // all permissions (but investigate whether this will nuke + // the all-permission optimizations in the JRE). + + // todo: does not exit app if close button pressed on JFrame + // with CLOSE_ON_EXIT (or whatever) set; if doesn't exit, use an + // WindowListener to catch WindowClosing event, then if exit is + // called immediately afterwards from AWT thread. + + // todo: deny all permissions to applications that should have + // already been 'shut down' by closing their resources and + // interrupt the threads if operating in a shared-VM (exit class + // set). Deny will probably will slow checks down a lot though. + + // todo: weak remember last getProperty application and + // re-install properties if another application calls, or find + // another way for different apps to have different properties + // in java.lang.Sytem with the same names. + + /** only class that can exit the JVM, if set */ + private Object exitClass = null; + + /** this exception prevents exiting the JVM */ + private SecurityException closeAppEx = // making here prevents huge stack traces + new SecurityException(R("RShutdown")); + + /** weak list of windows created */ + private WeakList<Object> weakWindows = new WeakList<Object>(); + + /** weak list of applications corresponding to window list */ + private WeakList<ApplicationInstance> weakApplications = + new WeakList<ApplicationInstance>(); + + /** Sets whether or not exit is allowed (in the context of the plugin, this is always false) */ + private boolean exitAllowed = true; + + /** + * Creates a JNLP SecurityManager. + */ + public JNLP3SecurityManager() { + } + + /** + * Returns whether the exit class is present on the stack, or + * true if no exit class is set. + */ + public boolean isExitClass() { + return isExitClass(getClassContext()); + } + + /** + * Returns whether the exit class is present on the stack, or + * true if no exit class is set. + */ + private boolean isExitClass(Class<?> stack[]) { + if (exitClass == null) { + return true; + } + + for (int i = 0; i < stack.length; i++) { + if (stack[i] == exitClass) { + return true; + } + } + + return false; + } + + /** + * Set the exit class, which is the only class that can exit the + * JVM; if not set then any class can exit the JVM. + * + * @param exitClass the exit class + * @throws IllegalStateException if the exit class is already set + */ + public void setExitClass(Class<?> exitClass) throws IllegalStateException { + if (this.exitClass != null) { + throw new IllegalStateException(R("RExitTaken")); + } + + this.exitClass = exitClass; + } + + /** + * Return the current Application, or null if none can be + * determined. + */ + public ApplicationInstance getApplication() { + return getApplication(Thread.currentThread(), getClassContext(), 0); + } + + /** + * Return the application the opened the specified window (only + * call from event dispatch thread). + */ + protected ApplicationInstance getApplication(Object window) { + for (int i = weakWindows.size(); i-- > 0;) { + Object w = weakWindows.get(i); + if (w == null) { + weakWindows.remove(i); + weakApplications.remove(i); + } + + if (w == window) { + return weakApplications.get(i); + } + } + + return null; + } + + /** + * Return the current Application, or null. + */ + protected ApplicationInstance getApplication(Thread thread, Class<?> stack[], int maxDepth) { + ClassLoader cl; + JNLPClassLoader jnlpCl; + + cl = thread.getContextClassLoader(); + while (cl != null) { + jnlpCl = getJnlpClassLoader(cl); + if (jnlpCl != null && jnlpCl.getApplication() != null) { + return jnlpCl.getApplication(); + } + cl = cl.getParent(); + } + + if (maxDepth <= 0) { + maxDepth = stack.length; + } + + // this needs to be tightened up + for (int i = 0; i < stack.length && i < maxDepth; i++) { + cl = stack[i].getClassLoader(); + while (cl != null) { + jnlpCl = getJnlpClassLoader(cl); + if (jnlpCl != null && jnlpCl.getApplication() != null) { + return jnlpCl.getApplication(); + } + cl = cl.getParent(); + } + } + return null; + } + + /** + * Returns the JNLPClassLoader associated with the given ClassLoader, or + * null. + * @param cl a ClassLoader + * @return JNLPClassLoader or null + */ + private JNLPClassLoader getJnlpClassLoader(ClassLoader cl) { + // Since we want to deal with JNLPClassLoader, extract it if this + // is a codebase loader + if (cl instanceof JNLPClassLoader.CodeBaseClassLoader) { + cl = ((JNLPClassLoader.CodeBaseClassLoader) cl).getParentJNLPClassLoader(); + } + + if (cl instanceof JNLPClassLoader) { + JNLPClassLoader loader = (JNLPClassLoader) cl; + return loader; + } + + return null; + } + + /** + * Returns the application's thread group if the application can + * be determined; otherwise returns super.getThreadGroup() + */ + @Override + public ThreadGroup getThreadGroup() { + ApplicationInstance app = getApplication(); + if (app == null) { + return super.getThreadGroup(); + } + + return app.getThreadGroup(); + } + + /** + * Throws a SecurityException if the permission is denied, + * otherwise return normally. This method always denies + * permission to change the security manager or policy. + */ + @Override + public void checkPermission(Permission perm) { + String name = perm.getName(); + + // Enable this manually -- it'll produce too much output for -verbose + // otherwise. + // if (true) + // OutputController.getLogger().log("Checking permission: " + perm.toString()); + + if (!JNLPRuntime.isWebstartApplication() && + ("setPolicy".equals(name) || "setSecurityManager".equals(name))) { + throw new SecurityException(R("RCantReplaceSM")); + } + + try { + // deny all permissions to stopped applications + // The call to getApplication() below might not work if an + // application hasn't been fully initialized yet. + // if (JNLPRuntime.isDebug()) { + // if (!"getClassLoader".equals(name)) { + // ApplicationInstance app = getApplication(); + // if (app != null && !app.isRunning()) + // throw new SecurityException(R("RDenyStopped")); + // } + // } + + super.checkPermission(perm); + } catch (SecurityException ex) { + OutputController.getLogger().log("Denying permission: " + perm); + throw ex; + } + } + + /** + * Checks whether the window can be displayed without an applet + * warning banner, and adds the window to the list of windows to + * be disposed when the calling application exits. + */ + @SuppressWarnings("deprecation") + @Override + public boolean checkTopLevelWindow(Object window) { + ApplicationInstance app = getApplication(); + + // remember window -> application mapping for focus, close on exit + if (app != null ) { + OutputController.getLogger().log(OutputController.Level.ERROR_DEBUG, "SM: app: " + app.getTitle() + " is adding a window: " + window + " with appContext " + App3Context.getAppContext()); + + weakWindows.add(window); // for mapping window -> app + weakApplications.add(app); + + app.addWindow(window); + } + + // todo: set awt.appletWarning to custom message + // todo: logo on with glass pane on JFrame/JWindow? + + return super.checkTopLevelWindow(window); + } + + /** + * Checks whether the caller can exit the system. This method + * identifies whether the caller is a real call to Runtime.exec + * and has special behavior when returning from this method + * would exit the JVM and an exit class is set: if the caller is + * not the exit class then the calling application will be + * stopped and its resources destroyed (when possible), and an + * exception will be thrown to prevent the JVM from shutting + * down.<p> + * + * Calls not from Runtime.exit or with no exit class set will + * behave normally, and the exit class can always exit the JVM. + */ + @Override + public void checkExit(int status) { + + // applets are not allowed to exit, but the plugin main class (primordial loader) is + Class<?> stack[] = getClassContext(); + if (!exitAllowed) { + for (int i = 0; i < stack.length; i++) { + if (stack[i].getClassLoader() != null) { + throw new AccessControlException("Applets may not call System.exit()"); + } + } + } + + super.checkExit(status); + + boolean realCall = (stack[1] == Runtime.class); + + if (isExitClass(stack)) { + return; + } // to Runtime.exit or fake call to see if app has permission + + // not called from Runtime.exit() + if (!realCall) { + // apps that can't exit should think they can exit normally + super.checkExit(status); + return; + } + + // but when they really call, stop only the app instead of the JVM + ApplicationInstance app = getApplication(Thread.currentThread(), stack, 0); + if (app == null) { + throw new SecurityException(R("RExitNoApp")); + } + + app.destroy(); + + throw closeAppEx; + } + + public void disableExit() { + exitAllowed = false; + } + + /** + * Tests if a client can get access to the AWT event queue. This version allows + * complete access to the EventQueue for its own AppContext-specific EventQueue. + * + * FIXME there are probably huge security implications for this. Eg: + * http://hg.openjdk.java.net/jdk7/awt/jdk/rev/8022709a306d + * + * @exception SecurityException if the caller does not have + * permission to accesss the AWT event queue. + */ + @SuppressWarnings("deprecation") + @Override + public void checkAwtEventQueueAccess() { + /* + * this is the templace of the code that should allow applets access to + * eventqueues + */ + + // AppContext appContext = AppContext.getAppContext(); + // ApplicationInstance instance = getApplication(); + + // if ((appContext == mainAppContext) && (instance != null)) { + // If we're about to allow access to the main EventQueue, + // and anything untrusted is on the class context stack, + // disallow access. + super.checkAwtEventQueueAccess(); + // } + } + +} |