Howto package Jython for Java WebStart/Applets

It's nice to use jython especially if you don't quite know what kind of computation you need to do or what kind of UI layout might be good... and jyhton is much easier on the eye than some XML markup ... or even a GUI builder.

But once part of your program is in jython how to you (easily) deliver it to the masses? Here we start with packaging a jython program for use with java webstart, later we'll look at other packaging options. Here is the Launcher : . And here is the (very simple) jython code we a going to turn into this webstart program (run.py):
import sys
# this is essestial
sys.path.append('__pyclasspath__/Lib')
# fire up the java console to see this output
print sys.path
print sys.argv
# os is partially python code
import os
# smtplib is our "non-trivial" python library using the socket library etc.
import smtplib
from java import awt
from java.lang import Runnable
from javax import swing

def doemail(address,msg):
        frm='jython@rubbish.com'
        to=address
        subject='Hello from the jython app' 
        headers='From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n' % (frm,to,subject)
        m=smtplib.SMTP(to.split('@')[1])
        try:
            m.sendmail(frm,[address],headers+str(msg))
            msguser('email sent!')
        finally:
            m.quit()

def sendmail(e):
    if email.text:
        doemail(email.text,msg.text)
    else:
    	msguser('input an email address first')
def msguser(msg):
    swing.JOptionPane.showMessageDialog(frame,msg,
    "info",swing.JOptionPane.INFORMATION_MESSAGE)
    
def exit(): raise SystemExit

def run():
    frame=swing.JFrame('A Jython swing app',windowClosing=lambda e:exit())
    email=swing.JTextField("",20)
    label=swing.JLabel('enter your email:',labelFor=email)
    msg=swing.JTextArea()

    top=swing.JPanel(awt.FlowLayout(awt.FlowLayout.LEFT))
    button = swing.JButton("send",actionPerformed=lambda e:sendmail(e))
    top.add(label)
    top.add(email)
    cp=frame.contentPane
    cp.add(top,awt.BorderLayout.NORTH)
    cp.add(msg,awt.BorderLayout.CENTER)
    cp.add(button,awt.BorderLayout.SOUTH)

    frame.bounds = (200,200,400,400)
    frame.visible = 1

class R(Runnable):
    def run(self): run()

# okay just run ....
swing.SwingUtilities.invokeLater(R())
We place this file in a directory called mypkg and add a __init__.py to turn it into a "package". Here is the important fragment of the jnlp file that is used to kick-off the webstart
<?xml version="1.0" encoding="utf-8"?>
<jnlp
  spec="1.5+"
  codebase="http://www.ce4csb.org/pub/"
  href="jython-test.jnlp">
  <information>
    <title>Jython Launcher</title>
    <vendor>Computational Systems Biology</vendor>
    <homepage href="http://www.ce4csb.org/" />
    <description>JythonLauncher Application</description>
    <icon               href="http://www.jython.org/css/jython.png"/>
    <icon kind="splash" href="http://www.jython.org/css/jython.png"/>
    <offline-allowed/> 
    <shortcut online="false">
      <desktop/>
      <menu submenu="Computational Systems Biology"/>
    </shortcut>
    <related-content href="http://creativecommons.org/licenses/by-nc-nd/3.0/" >
	   <title>Licence</title>
	   <description>This product is licensed under a Creative Commons License</description>
	   <icon href="http://i.creativecommons.org/l/by-nc-nd/3.0/88x31.png" />
    </related-content>
  </information>

  <security>
      <all-permissions/>
  </security>
  
<resources> <j2se version="1.5+"/> <!-- turn this part into a resource --> <jar href="http://www.ce4csb.org/jnlp/jython/jython2.5.1.jar" part="jython" /> <jar href="http://www.ce4csb.org/jnlp/jython/jythonlib2.5.1.jar" part="jython-lib" download="eager" /> <!-- see below --> <jar href="http://www.ce4csb.org/jnlp/jython/pychrome.jar" download="eager" /> <package name="org.python.*" part="jython" recursive="true" /> <package name="com.ziclix.python.sql.*" part="jython" recursive="true" /> <jar href="mypkg.jar" part="jython-code" download="eager" /> <!-- <property name="python.cachedir" value="${user.home}/.jythonhome/cachedir" /> --> </resources> <application-desc main-class="org.python.util.jython"> <argument>-c</argument> <argument>from mypkg import run</argument> </application-desc>
</jnlp>
This small test has 3 resources. The jython jar file, a jar of the "Lib" directory and a jar of the jython "program" you have written. (Notice too that they don't have to reside in the same directory... I have placed my jython.jar etc. jarfiles at a URL managed by a JnlpDownloadServlet to take advantage of the extra compression available with this servlet)

Packaging the Python Library

Package the Python Library. We remove all tests but you could go further since the resulting jar file for jython 2.5.1 is 3.9Mb! Do we really need all those encodings?
jythonlib:
    cp -R `jython -c 'import sys;print sys.prefix'`/Lib . # make a local copy
    # remove any "test" code
    /bin/rm -rf Lib/distutils Lib/test Lib/email/test # remove all tests - there's a lot!
    find Lib -name '*test*' -exec rm -f {} \; # plus anything that looks like a test!
    find Lib -name '*tests*' -exec rm -f {} \;
    @jython -c 'from compileall import main; main()' -d Lib Lib # now compile
    # get rid of python now
    find Lib -name '*.py' -exec rm -f {} \; # don't need the *.py files
    ${JAVA_HOME}/bin/jar cvf jythonlib.jar Lib
    /bin/rm -rf Lib

Signing your jar files

Because of code like this in the startup of PySystemState() constructor:

currentWorkingDir = new File("").getAbsolutePath();
You will get a java.security.AccessControlException: access denied (java.util.PropertyPermission user.dir read) exception unless all your jar files are signed. Sigh! Thus it is impossible at the moment to use jython in a applet (since the demise of jythonc at least). Happily it's not too hard to "sign" a jar once you have keystore

${JDK_HOME}/bin/jarsigner --keystore ${key.storefile} --storepass ${key.storepass}  \
	--keypass ${key.storepass}  jarfile-to-sign  ian.castleden
Of course you have to create a keystore file... which is a real pain (Of course once you have created it you can reuse it again and again....).
${JDK_HOME}/bin/keytool -genkey -keystore ${key.storefile} -keypass ${key.storepass} -storepass ${key.storepass} \ 
	-validity 3650 -keyalg RSA -alias ian.castleden \
	-dname "CN=www.ce4csb.org, OU=Computational Systems Biology, O=CPEB, L=Perth, ST=Western Australia, C=AU"
There is even an ANT Task to help:
    <target name="makeKeyStore">
    	<delete file="${key.storefile}"/>
    	<genkey keystore="${key.storefile}" keypass="${key.pass}"
    	   validity="3650" storepass="${key.storepass}" keyalg="RSA"
    	   alias="ian.castleden" verbose="true">   	
            <dname>
               <param name="CN" value="www.ce4csb.org"/>
               <param name="OU" value="Computational Systems Biology"/>
               <param name="O"  value="Plant Energy Biology"/>
               <param name="L"  value="Perth"/>
               <param name="ST" value="Western Australia"/>
               <param name="C"  value="AU"/>
            </dname>
    	</genkey>
    </target>
It is easiest if everything goes into a python package... say mypkg. Here is Makefile fragment that creates mysql.jar.
mypkg:
    @jython -c 'from compileall import main; main()' mypkg/
    jar cvf mypkg.jar mypkg/
    ${JDK_HOME}/bin/jarsigner --keystore ${key.storefile} --storepass ${key.storepass}  \
	--keypass ${key.storepass}  mypkg.jar  ian.castleden

Dealing with resources

One may have to package resources such as "icons" or configuration files. How does one reference them once they are in a jarfile? At first I thought something like this might work even though you have to supply the name of the jarfile "by hand"
from java.net import URL
def findurl(jarfile):
    import re
    _m=re.compile(r'^__pyclasspath__/(.*)/[^/]+\$py\.class$').match(__file__)
    if _m:
        U='jar:file:%s!/%s/' % (jarfile,_m.group(1))
        def mkurl(png): return URL(U+png)
    else:
        U,_=os.path.split(__file__)
        def mkurl(png): return URL('file:'+U+os.sep+png)
    return mkurl

mkurl=findurl('mypkg.jar')
def icon(png):
    png += '.png'
    url=mkurl(png)
    return swing.ImageIcon(url)
This does indeed work for example when launching from a shell where the jarfile is indeed mypkg.jar
#!/bin/sh
exec java -cp mypkg.jar:${JNLP}/jython2.5.1.jar:${JNLP}/jythonlib2.5.1.jar org.python.util.jython -c 'from mypkg import run' "$@"
But not from java webstart where the jarfile has some mangled name. After fruitlessly trying to get hold of the class loader used to load the jython code - in jython - I created a small wrapper (that could easily go into the jython codebase :) to do it for me.
package org.ce4csb.python;
// maybe package org.python.util ?;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

import org.python.core.ClasspathPyImporter;
import org.python.core.Py;
import org.python.core.PyObject;
import org.python.core.util.FileUtil;

public class ChromeLoader {
	/** uses the ClasspathPyImporter classLoader to 
	 * load this resource. Objects packaged up for as *.py or their
	 * class equivalents are extracted using this loader. We do the samething
	 * for other object jar'ed with them
	 * @param chrome the object image file/document to load
	 * @return input stream of object or null
	 */
	public static InputStream load(String chrome) {
		ClassLoader loader = ClasspathPyImporter.class.getClassLoader();
		if (loader != null) {
			return loader.getResourceAsStream(chrome);
			
		}
		return null;
	}
	public static URL findURL(String chrome) {
		ClassLoader loader = ClasspathPyImporter.class.getClassLoader();
		if (loader != null) {
			return loader.getResource(chrome);
		}
		return null;
	}
	public static byte[] loadBytes(String chrome) throws IOException {
		InputStream input = load(chrome);
		if (input == null) { return null; }
		return FileUtil.readBytes(input);
	}
	public static PyObject loadAsFile(String chrome) {
		InputStream input = load(chrome);
		if (input == null) { return Py.None; }
		return FileUtil.wrap(input);
	}
	
}
With this class one can get at resources with has:
def findurl():
    import re
    _m=re.compile(r'^__pyclasspath__/(.*)/[^/]+\$py\.class$').match(__file__)
    if _m:
        from org.ce4csb.python import ChromeLoader
        PKG=_m.group(1)
        def mkurl(png): return ChromeLoader.findURL(PKG + '/'+ png)
    else:
    	from java.net import URL
        PKG,_=os.path.split(__file__)
        def mkurl(png): return URL('file:'+PKG+os.sep+png)
    return mkurl

mkurl=findurl()
def icon(png):
    png += '.png'
    return swing.ImageIcon(mkurl(png))

TODO

Where does jython under webstart store its cachedir?
logos The Government of Western Australia The University of Western Australia Australian Research Council Centre of Excellence in Plant Energy Biology