Java App Bundlers Review

  • Tutorial
So, last time I wrote about a tool for building JavaFXPackager applications. There were 2 some ways to build the application, but none of them could be conveniently called simply from the code. But we are true Java programmers. And for such tru-programmers from version 8u20, a special API was created in JDK in JavaFXPackager, which allows you to simply take and assemble the bundle from your binaries like this. One problem is that this API is undocumented. But it’s not a problem, we’ll figure it out.

Sources of information


The primary source will, of course, be JavaDoc, which, however, is also not publicly available. Therefore, we will collect it ourselves from the source . Without forcing readers to do this, I will simply post it: ginz.bitbucket.org/fxpackager-javadoc . Of course, it will be useful to have the source code, as the API is poorly documented.

Introduction


Which class makes you pay attention first of all? Apparently, the Bundler interface with this specification:

// action methods
File execute(Map params, File outputParentDir);
boolean validate(Map params);
// information methods
Collection> getBundleParameters();
String getBundleType();
String getDescription();
String getID();
String getName();


The methods are clearly divided into 2 types: methods that perform actions with already given parameters and methods that provide information about this bundler.

execute accepts Map parameters and the output directory as parameters and creates a bundle in this directory in some expected format. But we can only expect correct execution if validate passes without exceptions, and it can throw them in 2 types: UnsupportedPlatformException and ConfigException, whose spoken names suggest that if you throw the first one, you simply use a bundler that is not supported by your platform. The second one is thrown if you have something wrong with the parameters passed.

And what can be passed as parameters to these methods in general? Most of the keys in these methods are in all kinds of static instances of the class.BundlerParamInfo , most of which (common cross-platform parameters) are in the StandardBundlerParam class , and more specific (platform-specific) are usually found in the Bundler implementation classes themselves : LinuxAppBundler , WinExeBundler and so on. Although it is not entirely obvious how this class is generally related to what we put in params, which we pass to execute. In fact, it turns out that the id obtained by getID () is the key in this Map, and the value must be of the type that BundlerParamInfo is parameterized.

And how to get instances of bundlers?


The easiest method to obtain all possible "preset» Bundler 's through the static method createBundlersInstance interface bundlers (Warning, the HOT: the Java 8):

public static List getSuitableBundlers() {
    return Bundlers.createBundlersInstance()
            .getBundlers()
            .stream()
            .filter(bundler -> {
                try {
                    bundler.validate(Collections.emptyMap());
                } catch (UnsupportedPlatformException ex) {
                    return false;
                } catch (ConfigException ignored) {
                }
                return true;
            }).collect(Collectors.toList());
}


Thus, we not only received all the “pre-installed” bundlers, but also filtered them according to the platform principle: as a rule, it is impossible to collect packages for one platform on another.

And what, no examples ?!


And, for sure, you can try to stop theorizing and draw some kind of example.

Since it seems to me that static typing is always better, we can wrap the creation of an associative array of parameters in something like BunderParamsBuilder:

public class BundlerParamsBuilder {
    private Map params = new HashMap<>();
    public  BundlerParamsBuilder setParam(BundlerParamInfo param, T value) {
        params.put(param.getID(), value);
        return this;
    }
    public BundlerParamsBuilder unsafeSetParam(String key, Object value) {
        params.put(key, value);
        return this;
    }
    public Map build() {
        return new HashMap<>(params);
    }
}

Thus, the assembly of the project will be approximately as follows (the existence of the running /tmp/helloWorld.jar is implied):

List bundlers = getSuitableBundlers();
Path directoryWithBundles = Files.createTempDirectory("bundles");
Path jar = Paths.get("/tmp/helloWorld.jar");
RelativeFileSet mainJar = new RelativeFileSet(jar.getParent().toFile(), new HashSet(
        Arrays.asList(jar.toFile())
));
Map params = new BundlerParamsBuilder()
        .setParam(StandardBundlerParam.APP_NAME, "HelloWorld")
        .setParam(StandardBundlerParam.APP_RESOURCES, mainJar)
        .build();
bundlers.forEach(bundler -> bundler.execute(params, directoryWithBundles.toFile()));
System.out.println("Bundles are created in " + directoryWithBundles);
System.out.println("Parameters after bundling: " + params);

The exhaust after launch will be approximately the following:
Bundles are created in / tmp / bundles5791581710818077755
Parameters after bundling: {appVersion = 1.0, copyright = Copyright (C) 2015, stopOnUninstall = true, .mac-jdk.runtime.rules = [Lcom.oracle.tools.packager.JreUtils $ Rule; @ 4c3e4790, mac.app. bundler = Mac Application Image, linux.deb.imageRoot = / tmp / fxbundler6592356981290936843 / images / linux-deb.image / helloworld-1.0 / opt, buildRoot = / tmp / fxbundler6592356981290936843, mac.bundle-id-signing-prefix = , linux.deb.licenseText = Unknown, linux.deb.maintainer = Unknown , jvmProperties={}, mac.signing-key-user-name=, licenseFile=[], identifier=HelloWorld, linux.rpm.imageDir=/tmp/fxbundler6592356981290936843/images/linux-rpm.image, runtime=RelativeFileSet{basedir:/home/dginzburg/soft/jdk1.8.0_25/jre, files:[]}, shortcutHint=false, mainJar=RelativeFileSet{basedir:/tmp, files:[helloWorld.jar]}, jvmOptions=[], name.fs=HelloWorld, fxPackaging=false, name=HelloWorld, appResources=RelativeFileSet{basedir:/tmp, files:[helloWorld.jar]}, mac.category=Unknown, linux.deb.imageDir=/tmp/fxbundler6592356981290936843/images/linux-deb.image/helloworld-1.0, .mac.default.icns=GenericAppHiDPI.icns, runAtStartup=false, linux.app.bundler=Linux Application Image, mac.signing-key-developer-id-app=null, linux.launcher.url=jar:file:/home/dginzburg/soft/jdk1.8.0_25/lib/ant-javafx.jar!/com/oracle/tools/packager/linux/JavaAppLauncher, description=HelloWorld, configRoot=/tmp/fxbundler6592356981290936843/macosx, preferencesID=HelloWorld, title=HelloWorld, linux.bundleName=helloworld, startOnInstall=false, mac.pkg.packagesRoot=/tmp/fxbundler6592356981290936843/packages, licenseType=Unknown, linux.deb.fullPackageName=helloworld-1.0, mac.CFBundleIdentifier=HelloWorld, serviceHint=false, vendor=Unknown, email=Unknown, applicationCategory=Unknown, mac.app.imageRoot=/tmp/fxbundler6592356981290936843/images/dmg.image, userJvmOptions={}, classpath=, linux.deb.configDir=/tmp/fxbundler6592356981290936843/images/linux-deb.image/helloworld-1.0/DEBIAN, verbose=false, imagesRoot=/tmp/fxbundler6592356981290936843/images, mac.daemon.image=/tmp/fxbundler6592356981290936843/images/pkg.daemon, applicationClass=HelloWorld, .linux.runtime.rules=[Lcom.oracle.tools.packager.JreUtils$Rule;@38cccef, menuHint=true}


Oh, where did it all come from here? We kind of put in params only 2 parameters. To understand this, we must look at sortsy method BunderParamInfo.fetchFrom , which is used to get the value of params:

//...
if (getDefaultValueFunction() != null) {
    T result =  getDefaultValueFunction().apply(params);
    if (result != null) {
        params.put(getID(), result);
    }
    return result;
}
//...

Yeah, if the parameter was not found in params and it can be obtained through other parameters (the defaultValueFunction function is responsible for this), then the value obtained in this way is crammed into params. For example, we did not specify the MAIN_JAR parameter, but MAIN_JAR is required in order to create a executable file. Let's look at how MAIN_JAR is defined: as defaultValueFunction we see:

params -> {
    extractMainClassInfoFromAppResources(params);
    return (RelativeFileSet) params.get("mainJar");
}

Here is the answer, where does the mainJar value come from.

Full list of options


In order not to search for parameters every time, I made a plate in which I displayed information on all found static instances of BundlerParamInfo.

Conclusion


The very minimum and most obvious features are described, so feel free to read the JavaDoc and even the code.
The article will be improved and corrected at the request of commentators.

Also popular now: