tschulte / gradle-jnlp-plugin

Gradle plugin to generate jnlp files, sign jars etc. for being able to start an application with Java Webstart
Apache License 2.0
20 stars 7 forks source link

Explain how the jnlp plugin is meant to be used in a war project #45

Open mauromol opened 7 years ago

mauromol commented 7 years ago

In my own Gradle multiproject, I ended up with this project structure:

The ria project applies the application and jnlp plugins, defines all the configuration needed to generate the JNLP file and the signed JARs. The war project applies the war project and simply decorates the war task:

def createWebstartDirTask = project(':ria').createWebstartDir
war {
    into('my-ria-web-folder') {
        from createWebstartDirTask
    }
}

(please note, in my actual project I also add a similar decoration to eclipseWtpComponent to include the packed ria application in Eclipse WTP project files, too, but it's not relevant to this reporting).

However, in your versionBasedAndPack200EnabledMinimalWebstart example you suggest you may apply the JNLP plugin directly to the war project. Correct me if I'm wrong, but I understand you may specify the JNLP dependencies through the newly defined jnlp configuration (while in a project that applies the application plugin dependencies are taken directly from the runtime configuration). However, since the jnlp plugin automatically makes the jnlp configuration extend runtime, you have to include this:

configurations {
    jnlp {
        extendsFrom = []
    }
}

This path would allow to pack a RIA which is not defined in the same Gradle multiproject (the dependency added to the jnlp configuration may indeed be a standard external dependency) and move the problem to generate the JNLP and signed JARs to the war project (enabling different packaging of the same RIA application on different web applications). However, I encountered another problem. Event if you redefine the jnlp configuration to extend from "nothing" ([]), which in any case sounds like a not-so-elegant requirement, looking at the source code I also see that the whole project the jnlp plugin is applied to is added to the jnlp dependencies (see: https://github.com/tschulte/gradle-jnlp-plugin/blob/develop/gradle-jnlp-plugin/src/main/groovy/de/gliderpilot/gradle/jnlp/GradleJnlpPlugin.groovy#L52). This means that the whole war project classpath is added to the jnlp configuration (through transitive dependencies), which does not sound correct to me and makes me wonder whether making the jnlp configuration extend the runtime configuration is indeed needed or not.

IMHO, the whole block at https://github.com/tschulte/gradle-jnlp-plugin/blob/develop/gradle-jnlp-plugin/src/main/groovy/de/gliderpilot/gradle/jnlp/GradleJnlpPlugin.groovy#L48 should be applied only if the project does not also apply the war plugin.

What do you think?

tschulte commented 7 years ago

Yes, documentation is lacking and sometimes even outdated. I am sorry for that. I really need to work on that.

There exists a plugin de.gliderpilot.jnlp-war. This is not really documented yet. It contains a new implementation of the demo webstart servlet and supports pack200 and even jardiff (although jardiff support is not tested yet).

The easiest way to go is to use the jnlp-war plugin. Your project structure is OK.

In your ria subproject:

apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'de.gliderpilot.jnlp'
[...]

In your war subproject

apply plugin: 'de.gliderpilot.jnlp-war'
repositories {
    // needed for the jnlp-servlet
    jcenter()
}
jnlpWar {
    from project(':ria')
}

What this does is as follows:

The final layout of the war file is

META-INF/
META-INF/MANIFEST.MF
WEB-INF/
WEB-INF/lib/
WEB-INF/lib/jnlp-servlet-1.2.2.jar
WEB-INF/lib/javax.servlet-api-3.1.0.jar
WEB-INF/lib/javax.websocket-api-1.0.jar
launch.jnlp
lib/
lib/ria__V1.0.0.jar.pack.gz

The jnlp-war plugin also allows jardiff download, but that is not production ready yet, might not work and needs some further setup:

ria/build.gradle:

apply plugin: 'maven-publish'
publishing {
    repositories {
        maven {
            url "..."
        }
    }
    publications {
        mavenJava(MavenPublication) {
            artifact webstartDistZip {
                classifier "webstart"
            }
        }
    }
}

war/build.gradle:

repositories {
    maven {
        url "..."
    }
}
jnlpWar {
    from rootProject
    versions {
        "1.0.0" "$group:ria:1.0.0:webstart@zip"
    }
    launchers {
        "$version" {
            jardiff {
                from "1.0.0"
            }
        }
    }
}

The above will also put jardiff files to upgrade from v1.0 to e.g. v1.1 of your application with minimizing the download by using the jardiff protocol. The resulting war has the following content:

META-INF/
META-INF/MANIFEST.MF
WEB-INF/
WEB-INF/lib/
WEB-INF/lib/jnlp-servlet-1.2.2.jar
WEB-INF/lib/javax.servlet-api-3.1.0.jar
WEB-INF/lib/javax.websocket-api-1.0.jar
launch.jnlp
lib/
lib/ria__V1.1.0.jar.pack.gz
lib/ria__V1.0.0__V1.1.0.diff.jar.pack.gz

The webstart client does support the jardiff protocoll. If version 1.0.0 has previously been used on the client, it would ask for an upgrade from v1.0.0 to v1.1.0. The servlet would in that case deliver the jardiff. The code is even intelligent enough to only use jardiff, if the jardiff is smaller than the new jar file. It also uses pack200, if the ria project uses pack200. It even checks if the diff.pack200 file is smaller than the diff file without pack200.

But again. The jardiff part of the jnlp-war plugin is not production ready yet -- the rest though is tested heavily by my employer). But I would be happy to have any beta testers for the jardiff part.

And again. Sorry for the lack of documentation. I really need to work on that.

mauromol commented 7 years ago

Hi Tobias, thank you very much. I think that some quick documentation like this would be very useful to be included here on GitHub in README files or such.

Just some more questions:

  • In war from project(':ria') does all the heavy lifting (removes the lines and jnlp.packEnabled from the jnlp again and sets codbase and href to $$href and $$name -- because all that is handled by the servlet). And de.gliderpilot.gradle.jnlp:jnlp-servlet:$jnlpVersion is automatically added.

I think you mean $$href and $$codebase. Anyway, I was wondering why you remove jnlp.versionEnabled and jnlp.packEnabled: may it be because your own servlet just requires the use of versions and pack200 in any case?

from rootProject

Shouldn't the above line in the second example be again the following?

from project(':ria')

And what do you think about my thoughts on the following?

IMHO, the whole block at https://github.com/tschulte/gradle-jnlp-plugin/blob/develop/gradle-jnlp-plugin/src/main/groovy/de/gliderpilot/gradle/jnlp/GradleJnlpPlugin.groovy#L48 should be applied only if the project does not also apply the war plugin.

tschulte commented 7 years ago

Yes, I meant $$href and $$codebase. And from project(':ria'). I copied that from our project, and we use rootProject.

jnlp.versionEnabled and jnlp.packEnabled are removed, because they are handled by the servlet instead of the JNLP client. The Feature of letting the client handle them was added in Java 6 or 7, I think. When the versionsEnabled property is set, the client will automatically request __Vx.y.z.jar files. When the packEnabled property is set, the client will in addition automatically request .jar.pack.gz files. This feature is meant to be used with static web sites.

The jnlp plugin is designed to support hosting the jnlp files on a static site. Which could be a war file without jnlp-servlet.

To also allow jardiff, you have to disable this client-side features again and use the jnlp-servlet. The servlet also allows replacement of href and codebase.

The sample project applying the jnlp plugin and the war plugin was a mistake. I don't think it is wise to do that. The problem is, that if you apply the war plugin, the java plugin is automatically applied as well. I don't know of a way to define if plugin java and not plugin war. The current line project.plugins.withId('java') {... } is a way to lazily do somethink if a plugin is applied. The code is run as soon as the plugin is applied. I could check inside with if (plugins.hasPlugin('war') {}, but that would not work if the project does

apply plugin: 'java'
apply plugin: 'war'

only if the first line was removed. The only reliable way would be to introduce a property in the jnlp extension to disable that. But again, I think it is best to not mix jnlp and war in the same project. The jnlp plugin is best applied to the same project as the application, not the war.

I will try to improve the documentation.

mauromol commented 7 years ago

jnlp.versionEnabled and jnlp.packEnabled are removed, because they are handled by the servlet instead of the JNLP client. The Feature of letting the client handle them was added in Java 6 or 7, I think. When the versionsEnabled property is set, the client will automatically request __Vx.y.z.jar files. When the packEnabled property is set, the client will in addition automatically request .jar.pack.gz files. This feature is meant to be used with static web sites.

Then I didn't understand this right. I thought those two properties were a mutual convention between the servlet and the client. Sorry for bothering you, but I'd like to be sure I've understood it thoroughly. What is working in my current setup is this:

This works (I tested with a couple of Java 8 Web Start clients).

Instead, if I set jnlp.versionEnabled and jnlp.packEnabled to false (so they are not present in the final JNLP file) and hence the generated JNLP contains jar elements with href values containing __Vx.y.z.jar suffixes but no version attributes, the client seems to produce plain requests with URLs like http://baseURL/ria/myjar__V1.0.0.jar and the JnlpDownloadServlet produces 404 errors. This puzzles me a bit.

Probably I'm just mixing concepts: I must use the JnlpDownloadServlet XOR use jnlp.versionEnabled and jnlp.packEnabled to enable versioning and packing for static websites. Things however are a bit complicated and mixed up by the fact that to produce the right result for using versioning and Pack200, I do must set jnlp.versionEnabled and jnlp.packEnabled to true in the jnlp block of the ria project (otherwise JARs are not packed with Pack200 and version attributes are not produced) BUT then use the jnlp-war plugin in the war project to strip out the corresponding elements from the generated JNLP file.

Am I understanding it right, now?

tschulte commented 7 years ago

The java 7 version of the documentation I linked in https://github.com/tschulte/gradle-jnlp-plugin/issues/43#issuecomment-320340796 is even more clear for this: http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/tools/pack200.html.

I think the JnlpDownloadServlet does just have an issue with files ending in __Vsomething.jar.

I strongly suggest giving the jnlp-war plugin and it's servlet a try. Or are there any features the servlet is missing?

mauromol commented 7 years ago

Hi Tobias, is there a way to tell the jnlp-war plugin to put everything into a subdirectory of the WAR? By default, the JNLP is put on the root of the WAR and the signed JARs in the lib subfolder of the WAR root. I would like to move both into the /ria subfolder (so that the JNLP is available at http://host:port/context/ria/launch.jnlp). I tried with:

jnlpWar {
  from project(':ria')
  launchesSpec.into('/ria')
}

but what I see is that instead of putting things into /ria, they are put into ria-webstart-1.0 (which resembles the name of the webstart distribution ZIP of the ria project).

tschulte commented 7 years ago

No, that is not possible. But why do you need to create one big jar? You can create one ria.war to have http://host:port/ria/launch.jnlp and a second war for the rest.

mauromol commented 7 years ago

No, that is not possible. But why do you need to create one big jar? You can create one ria.war to have http://host:port/ria/launch.jnlp and a second war for the rest.

I suppose you're thinking of a JEE server scenario (with an EAR and two WARs), but we work with Tomcat, hence we produce just a single WAR.

tschulte commented 7 years ago

No, I was not necessarily thinking of an JEE scenario with EARs containing two WARs. It should be possible to have multiple WARs deployed to Tomcat (e.g. https://stackoverflow.com/questions/28908895/deploy-multiple-wars-on-tomcat).

But nonetheless. You might want to create an issue and pull request for such a feature. It should be relatively easy to do so by introducing a new intermediate childSpec of the war task to be used instead of the war task directly. And it must be checked if the defaults for codebase and href are still correct.

mauromol commented 7 years ago

And it must be checked if the defaults for codebase and href are still correct.

I opened #47. I don't think anything in codebase and href defaults should be changed, as all the paths are relative to codebase and if you let it use $$codebase the JnlpDownloadServlet will take care of adding the subfolder name. With my own solution (which does not use the jnlp-war plugin) I'm currently embedding the JNLP and signed JARs in a subfolder with no issue.

tschulte commented 7 years ago

Yes, you are right. The defaults for codebase and href work OK. I just checked.

Is this issue the only thing hindering you to use the jnlp-war plugin and it's servlet?

mauromol commented 7 years ago

Right now it may be the only showstopper. However I could not do a complete test yet. The other problem may be the difficulty to use the jnlp-war plugin from the plugins.gradle.org (which is my preferred choice, if available).