Итак, в прошлый раз я писал об инструменте для сборки приложений JavaFXPackager. Там было 2 каких-то способа собрать приложение, но ни один из них не мог быть удобно вызван просто из кода. Но мы же труЪ Java-программисты. И вот для таких труЪ-программистов с версии 8u20 и был создан в JDK специальный API в JavaFXPackager, который позволяет просто вот так взять и собрать бандл из ваших бинарников. Одна проблема — этот API незадокументирован. Но не беда, разберёмся.

Источники информации


Первичным источником будет, естественно, JavaDoc, который, впрочем, тоже не доступен публично. Поэтому мы соберём его сами из исходников. Не заставляя это делать читателей, я просто выложу его: ginz.bitbucket.org/fxpackager-javadoc. Конечно, нелишним будет иметь у себя исходный код, так как документирован API плохо.

Введение


Какой класс заставляет в первую очередь обратить на себя внимание? Видимо, интерфейс Bundler с вот такой спецификацией:

// action methods
File execute(Map<String,? super Object> params, File outputParentDir);
boolean validate(Map<String,? super Object> params);

// information methods
Collection<BundlerParamInfo<?>> getBundleParameters();
String getBundleType();
String getDescription();
String getID();
String getName();


Методы явно делятся на 2 вида: методы, производящие действия с уже данными параметрами и методы, дающие информацию об этом бандлере.

execute принимает Map параметров и выходную директорию в качестве параметров и создаёт в данной директории бандл в каком-то ожидаемом формате. Но ожидать правильного исполнения мы можем только если validate проходит без исключений, а их он может кинуть 2 вида: UnsupportedPlatformException и ConfigException, говорящие названия которых подсказывают, что в случае выкидывания первого вы просто используете бандлер, который не поддерживается вашей платформой. Второй же выкидывается, если у вас что-то не так с переданными параметрами.

А что вообще можно передавать в качестве параметров в эти методы? Большая часть ключей в этих методах находится во всяких статических экземплярах класса BundlerParamInfo, большая часть из которых (общие кроссплатформенные параметры) находятся в классе StandardBundlerParam, а более специфичные (платформенно-зависимые), как правило, находятся в самих классах-реализациях Bundler'а: LinuxAppBundler, WinExeBundler и так далее. Хотя не совсем очевидно, каким образом этот класс вообще связан с тем, что мы кладём в params, который передаём execute. На самом деле оказывается, что id, получаемый getID(), является ключом в этом Map'е, а значение должно быть типа, которым параметризован BundlerParamInfo.

А как получить экземпляры бандлеров?


Самый простой метод получения всех возможных «предустановленных» Bundler'ов через статический метод createBundlersInstance интерфейса Bundlers (Warning, HOT: Java 8):

public static List<Bundler> 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());
}


Таким образом, мы не только получили все «предустановленные» бандлеры, но и отфильтровали их по платформенному принципу: как правило, невозможно собирать пакеты для одной платформы на другой.

И что, никаких примеров?!


А, точно, можно попробовать перестать теоретизировать и нарисовать какой-нибудь пример.

Поскольку мне кажется, что статическая типизация всегда лучше, то мы можем обернуть создание ассоциативного массива параметров во что-то типа BunderParamsBuilder:

public class BundlerParamsBuilder {
    private Map<String, Object> params = new HashMap<>();

    public <T> BundlerParamsBuilder setParam(BundlerParamInfo<T> 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<String, Object> build() {
        return new HashMap<>(params);
    }
}

Таким образом, сборка проекта будет заключаться примерно в следующем (подразумевается существование запускающегося /tmp/helloWorld.jar):

List<Bundler> bundlers = getSuitableBundlers();
Path directoryWithBundles = Files.createTempDirectory("bundles");
Path jar = Paths.get("/tmp/helloWorld.jar");

RelativeFileSet mainJar = new RelativeFileSet(jar.getParent().toFile(), new HashSet<File>(
        Arrays.asList(jar.toFile())
));
Map<String, Object> 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);

Выхлоп после запуска будет примерно следующий:
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=HelloWorld., linux.deb.licenseText=Unknown, linux.deb.maintainer=Unknown <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:[<very big list of 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}


Ой, откуда у нас всё это тут взялось? Мы вроде клали в params только 2 параметра. Чтобы разобраться в этом, надо посмотреть на сорцы метода BunderParamInfo.fetchFrom, который используется для того, чтобы получить значение параметра из params:

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

Ага, если параметр не был найден в params и он может быть получен через другие параметры (за это отвечает функция defaultValueFunction), то значение, полученное таким образом, запихивается в params. Например, мы не указали параметр MAIN_JAR, но MAIN_JAR обязателен для того, чтобы создать запускаемый файл. Посмотрим на то, как определяется MAIN_JAR: в качестве defaultValueFunction видим:

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

Вот и ответ, откуда там появляется значение mainJar.

Полный список параметров


Для того, чтобы не искать каждый раз параметры, я сделал табличку, в которую вывел информацию по всем найденным статическим экземплярам BundlerParamInfo.

Заключение


Описан самый минимум и самые очевидные возможности, так что не стесняйтесь читать JavaDoc и даже код.
Статья будет улучшаться и исправляться по запросу комментаторов.

Комментарии (0)