tschulte/gradle-jnlp-plugin

Enable JarDiff-Download using JnlpDownloadServlet

tschulte opened this issue · 2 comments

The plugin currently supports everything necessary to best support Java Webstart for Java 7 and newer without the need for a specialized download servlet (which was required prior Java 7 to achive the same functionality).

=> http://docs.oracle.com/javase/tutorial/deployment/deploymentInDepth/bestPractices.html

Part of the best practices mentioned in the above link are not in the scope of this plugin: Everything that has to be done in the HTML file must be done by the project using this plugin.

This is especially true for the codebase attribute. You either have to hard-code the codebase in the build.gradle to the value of the server, follow http://docs.oracle.com/javase/tutorial/deployment/deploymentInDepth/deployingWithoutCodebase.html (i.e. create a HTML file containing some javascript), or use the download servlet (which is not directly supported right now).

But one last piece is missing: the JnlpDownloadServlet does support JarDiff download to further reduce the download. This is not possible using just client side techniques available till Java 7.

The purpose of this issue can be split in two parts:

Investigate possibilities of Java Webstart regarding JarDiff/Pack200

  • What is JarDiff (File format, etc.)? It seems, the JarDiff is just a jar file (http://www.informit.com/articles/article.aspx?p=25044&seqNum=6)
  • Can JarDiff be combined with Pack200? Since a JarDiff is just a jar file, I would hope this to be true, but it must be checked if this is supported by the Webstart client
  • What gains can be expected

If it is worth it, implement JarDiff support

This only makes sense when creating a war file. Therefore, this would only work for war projects, i.e. projects with plugin 'war' applied

  • Automatically (maybe configured using the extension) add a dependency to the JnlpDownloadServlet
  • don't generate jnlp.packEnabled and jnlp.versionEnabled properties in the jnlp (this must be handled by the servlet instead of the client in that case)
  • Configure the old version(s) to also be included

Either by defining a dependency on both the new version and the old version(s) of the application

configurations {
    jnlpV1
    jnlpV2
    // ...
}
dependencies {
    jnlp 'group:artifact:v3'
    jnlpV1 'group:artifact:v1'
    jnlpV2 'group:artifact:v1'
}

The plugin would collect all dependencies from configurations which name startsWith 'jnlp' and put all the jars signed and packed and versioned in the lib directory.

Or by defining a dependency on both the new version of the application and the old version(s) of the war
This may be a special case of the above, but instead of defining a dependency for jnlpV1 on the application with an older version, there would be a dependency on an older version of the war. In that case the plugin would not have to sign and pack the jars, because the war does already contain signed and packed jars.

There exists a Stackoverflow question for this: http://stackoverflow.com/questions/15809410/can-jardiff-support-pack200-pack-gz

I have implemented a proof of concept Servlet implementing the JNLP Spec (http://download.oracle.com/otn-pub/jcp/jnlp-1.5-mr-spec-oth-JSpec/jnlp-1_5-mr-spec.pdf), with pack200 support and jardiff-pack200 support. Using this servlet I was able to update a griffon 2.1 app to griffon 2.2.

  • InitialDownload of the griffon 2.1 app was 4.4MB
  • Update to griffon 2.2 without jardiff: 2.3MB
  • Update to griffon 2.2 with jardiff-pack200: 1.1MB

Therefore I think it is worth it. Therefore we now have to have a look at the desired project layout of projects using the plugin.

First pending decision: The JnlpDownloadServlet does not support jardiff with pack200. It has some other limitations too:

  • You have to pack both the jar file and the pack200 file in the war for it to work. This does unnecessarily increase the size of the war.
  • Jardiff files are created on the fly by the servlet, but it does not support pack200.
  • The servlet has plenty of code dealing with JNLP 1.0 and 1.1.
  • I am not really able to follow the code and understand everything. My proof of concept servlet is just 92 loc (only handling jar files).
  • The servlet does handle both jar files and jnlp files. For JNLP files it does implement replacing parameters. This should most probably be separated in two servlets.

My proof of concept servlet does only need the jar.pack.gz files and the jardiff.pack.gz files (preprocessed during creation of the war file. This evening I will post my first ideas for changing the plugin to support this. I think this might result in some backward incompatible changes to the plugin.

I will most probably implement a new Servlet as a replacement for the old JnlpDownloadServlet (not exactly a drop in replacement, rather a servlet tightly coupled to this plugin -- or better said, a servlet requiring a special layout which can be conveniently created using this plugin).

Currently the plugin can be used in plenty of ways:

On a java application project

apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'distribution'
apply plugin: 'de.gliderpilot.jnlp'
mainClassName = 'org.example.Launcher'

jnlp {
    useVersions = true
    usePack200 = true
    withXml {
        information {
            title project.name
            vendor project.group ?: project.name
        }
        security {
            'all-permissions'()
        }
    }
    signJarParams = [alias: keyAlias, storepass: keystorePwd]
}

Using the above build script, a jnlp folder is created ready to be deployed on any HTTP server (since Java 7, see http://docs.oracle.com/javase/tutorial/deployment/deploymentInDepth/index.html). All you have to do is add a html site with some javascript.

Create a jnlp extension, applet or installer

apply plugin: 'java'
apply plugin: 'de.gliderpilot.jnlp'

jnlp {
    useVersions = true
    usePack200 = true
    desc = {
        'component-desc'()
    }
}

Again, this produces a jnlp folder ready to be deployed on any HTTP server. Using the desc-closure you can configure the type of the launcher (the default is 'application-desc()')

Create a jnlp folder in a separate project

apply plugin: 'de.gliderpilot.jnlp'

dependencies {
    jnlp project(':application')
}

jnlp {
    mainClassName = 'org.example.Launcher'
    [...]
}

Create a war file

apply plugin: 'war'
apply plugin: 'de.gliderpilot.jnlp'

dependencies {
    jnlp project(':application')
}

jnlp {
    mainClassName = 'org.example.Launcher'
    [...]
}

war {
    from createWebstartDir
}

But this last example has one issue: the jnlp-configuration does automatically extend configurations.runtime, if the java-plugin is applied. The war-plugin does automatically apply the java-plugin. Therefore this does also include all dependencies and classes of the war in the webstart application.

Proposed changes to the plugin

Encourage using the plugin only on java projects, discourage using it on war projects.

  • Remove the mainClassName option from the extension. If the application plugin is applied, this is used, otherwise a component-desc is assumed.
  • The plugin does already automatically export a distribution.zip if the distribution-plugin is applied.
  • Remove the jnlp configuration, just use runtime
  • enable useVersions by default
  • enable usePack200 by default(?) -- pack200 does not work when starting with javaws file://..., but I think, when you want to start from a network-share, just deploy a batch file or shell script.

Create a new JnlpWar-plugin

Configuration:

apply plugin: 'de.gliderpilot.jnlp-war'
jnlp {
    versions {
        v1 'org.example:application:1.0:webstart@zip'
        v2 'org.example:application:2.0:webstart@zip'
        v3 'org.example:application:3.0:webstart@zip'
    }
    launchers {
        from(v2) {
            rename 'launch.jnlp', 'launch_v2.jnlp'
            jardiff {
                from v1
            }
        }
        from(v3) {
            jardiff {
                from v1, v2
            }
        }
    }
}

This would automatically create a war file with the new servlet. It would contain one launch.jnlp from v3, one launch_v2.jnlp, all .jar.pack.gz (if usePack200 was used in the application project) from v3 and v2 and in addition the jardiff.jar.pack.gz to enable upgrade from v1 to v2 and v3 and to enable upgrade from v2 to v3. On copying the launch.jnlp files, the jnlp.versionEnabled and jnlp.packEnabled system properties are automatically removed, because version based download and pack200 support will be handled by the servlet and not the client.

One drawback: if useVersions is not used by the application, this does not work. If there are any unversioned files in the application (dependencies not retrieved using a maven-dependency), it does not work either.

But I really like to get some input.

@aalmiray, your tweet was the reason I created this plugin. And I think most of the downloads of the plugin from bintray are due to its inclusion in the griffon lazybones templates. Therefore I would especially like to hear your valuable feedback.